Control: tags -1 patch

Hi,

I am including a patch for this. Please forward it upstream. I do not
want to open an account on that website for a one-time submission.

I have tried this within the Production view in Tryton. If there is any
other use case, please let me know.
From: Bastian Germann <[email protected]>
Date: Sat, 31 Jan 2026 11:00:56 +0100
Subject: Port from GooCanvas to Cairo

Replace GooCanvas-based rendering with pure Cairo/Gtk.DrawingArea.
This removes the dependency on GooCanvas while maintaining the same
visual appearance and functionality.
---
--- a/README
+++ b/README
@@ -1,7 +1,7 @@
 GooCalendar
 ===========
 
-A calendar widget for GTK using GooCanvas
+A calendar widget for GTK
 
 Nutshell
 --------
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -1,15 +1,15 @@
-:mod:`goocalendar` --- Calendar widget using GooCanvas
-======================================================
+:mod:`goocalendar` --- Calendar widget
+======================================
 
 .. module:: goocalendar
-   :synopsis: Calendar widget using GooCanvas
+   :synopsis: Calendar widget
 .. moduleauthor:: Samuel Abels <http://debain.org>
 .. moduleauthor:: Cédric Krier <[email protected]>
 .. moduleauthor:: Antoine Smolders <[email protected]>
 .. sectionauthor:: Antoine Smolders <[email protected]>
 
-The :mod:`goocalendar <goocalendar>` module supplies a calendar widget drawed
-with GooCanvas that can display a month view and a week view. It also supplies
+The :mod:`goocalendar <goocalendar>` module supplies a calendar widget
+that can display a month view and a week view. It also supplies
 classes to manage events you can add to the calendar.
 
 
@@ -17,8 +17,8 @@ classes to manage events you can add to the calendar.
 
 Calendar Objects
 ------------------
-A :class:`Calendar <goocalendar.Calendar>` is a calendar widget using
-GooCanvas that can display a month view and a week view. It holds an
+A :class:`Calendar <goocalendar.Calendar>` is a calendar widget
+that can display a month view and a week view. It holds an
 :class:`EventStore<goocalendar.EventStore>` which contains events
 displayed in the calendar.
 
--- a/goocalendar/__init__.py
+++ b/goocalendar/__init__.py
@@ -4,10 +4,7 @@ import gi
 
 gi.require_version('Gtk', '3.0')
 gi.require_version('Gdk', '3.0')
-try:
-    gi.require_version('GooCanvas', '3.0')
-except ValueError:
-    gi.require_version('GooCanvas', '2.0')
+gi.require_version('PangoCairo', '1.0')
 from ._calendar import Calendar  # noqa: E402
 from ._event import Event, EventStore  # noqa: E402
 
--- a/goocalendar/_calendar.py
+++ b/goocalendar/_calendar.py
@@ -4,12 +4,13 @@ import calendar
 import datetime
 import math
 
-from gi.repository import Gdk, GObject, GooCanvas, Gtk, Pango
+import cairo
+from gi.repository import Gdk, GObject, Gtk, Pango, PangoCairo
 
 from . import util
 
 
-class Calendar(GooCanvas.Canvas):
+class Calendar(Gtk.DrawingArea):
     AVAILABLE_VIEWS = ["month", "week", "day"]
     MIN_PER_LEVEL = 15  # Number of minutes per graduation for drag and drop
 
@@ -75,14 +76,16 @@ class Calendar(GooCanvas.Canvas):
         self._day_width = 0
         self._day_height = 0
         self._event_items = []
+        self._tooltip_text = None
+        self._tooltip_x = None
+        self._tooltip_y = None
         assert view in self.AVAILABLE_VIEWS
         self.view = view
         self.selected_date = datetime.date.today()
         self.time_format = time_format
         self.min_width = self.min_height = 200
-        self.set_bounds(0, 0, self.min_width, self.min_height)
         self.set_can_focus(True)
-        self.set_events(
+        self.add_events(
             Gdk.EventMask.EXPOSURE_MASK
             | Gdk.EventMask.BUTTON_PRESS_MASK
             | Gdk.EventMask.BUTTON_RELEASE_MASK
@@ -93,24 +96,23 @@ class Calendar(GooCanvas.Canvas):
             | Gdk.EventMask.ENTER_NOTIFY_MASK
             | Gdk.EventMask.LEAVE_NOTIFY_MASK
             | Gdk.EventMask.FOCUS_CHANGE_MASK)
-        self.connect_after('realize', self.on_realize)
+        self.connect('realize', self.on_realize)
         self.connect('size-allocate', self.on_size_allocate)
         self.connect('key-press-event', self.on_key_press_event)
+        self.connect('draw', self.on_draw)
+        self.connect('button-press-event', self.on_button_press_event)
+        self.connect('button-release-event', self.on_button_release_event)
+        self.connect('motion-notify-event', self.on_motion_notify_event)
+        self.connect('query-tooltip', self.on_query_tooltip)
 
-        # Initialize background, timeline and days and add them to canvas
-        root = self.get_root_item()
-        self._bg_rect = GooCanvas.CanvasRect(parent=root, x=0, y=0,
-            stroke_color='white', fill_color='white')
-        self._timeline = TimelineItem(self, time_format=self.time_format)
-        root.add_child(self._timeline, -1)
+        # Initialize day and event data structures
         self.days = []
         while len(self.days) < 42:  # 6 rows of 7 days
             box = DayItem(self)
-            root.add_child(box, -1)
-            box.connect('button_press_event',
-                self.on_day_item_button_press_event)
             self.days.append(box)
 
+        self._timeline = TimelineItem(self, time_format=self.time_format)
+
     def do_set_property(self, prop, value):
         self.__props[prop.name] = value
 
@@ -182,11 +184,10 @@ class Calendar(GooCanvas.Canvas):
         new_day.full_border = True
 
         # Redraw.
-        old_day.update()
-        new_day.update()
         self._selected_day = new_day
         if old_day != new_day:
             self.emit('day-selected', self.selected_date)
+        self.queue_draw()
 
     def previous_page(self):
         cal = calendar.Calendar(self.firstweekday)
@@ -235,60 +236,57 @@ class Calendar(GooCanvas.Canvas):
 
     def on_realize(self, *args):
         self._realized = True
-        self.grab_focus(self.get_root_item())
+        self.grab_focus()
         self.on_size_allocate(*args)
 
     def on_size_allocate(self, *args):
         alloc = self.get_allocation()
         if not self._realized or alloc.width < 10 or alloc.height < 10:
             return
-        self.set_bounds(0, 0, alloc.width, alloc.height)
         self.update()
 
     def update(self):
         if not self._realized:
             return
         min_size = (self.min_width, self.min_height)
-        self.draw_background()
-        if self.view == "month":
-            self.draw_month()
-        elif self.view == "week":
-            self.draw_week()
-        elif self.view == "day":
-            self.draw_day()
-        self.draw_events()
+        self._prepare_layout()
         if min_size != (self.min_width, self.min_height):
             self.queue_resize()
+        self.queue_draw()
+
+    def _prepare_layout(self):
+        """Prepare the layout data for drawing."""
+        alloc = self.get_allocation()
+        w, h = alloc.width, alloc.height
 
-    def draw_background(self):
-        x, y, w, h = self.get_bounds()
-        self._bg_rect.set_property('width', w)
-        self._bg_rect.set_property('height', h)
+        if self.view == "month":
+            self._prepare_month_layout(w, h)
+        elif self.view == "week":
+            self._prepare_week_layout(w, h)
+        elif self.view == "day":
+            self._prepare_day_layout(w, h)
+        self._prepare_events()
 
-    def draw_day(self):
+    def _prepare_day_layout(self, w, h):
         """
-        Draws the currently selected day.
+        Prepares the layout for the currently selected day.
         """
-        x, y, w, h = self.get_bounds()
-        timeline_w = self._timeline.width
+        timeline_w = self._timeline.get_width(self)
         dayno = self.selected_date.weekday()
         day_name = calendar.day_name[dayno]
-        # Sum the needed space for the date before the day_name
         caption_size = len(day_name) + 3
         day_width_min = caption_size * self.font_size / Pango.SCALE
         day_width_max = (w - timeline_w)
         self._day_width = max(day_width_min, day_width_max)
         self._day_height = h
 
-        # Redraw all days.
+        # Prepare all days.
         cal = calendar.Calendar(self.firstweekday)
         weeks = util.my_monthdatescalendar(cal, self.selected_date)
         for weekno, week in enumerate(weeks):
-            # Hide all days that are not part of the current day
             for i, date in enumerate(week):
                 box = self.days[weekno * 7 + i]
-                box.set_property(
-                    'visibility', GooCanvas.CanvasItemVisibility.INVISIBLE)
+                box.visible = False
             if self.selected_date not in week:
                 continue
 
@@ -297,7 +295,6 @@ class Calendar(GooCanvas.Canvas):
             else:
                 the_body_color = self.props.body_color
 
-            # Draw.
             box = self.days[weekno * 7 + dayno]
             box.x = timeline_w
             box.y = 0
@@ -309,41 +306,33 @@ class Calendar(GooCanvas.Canvas):
             box.border_color = self.props.selected_border_color
             box.body_color = the_body_color
             box.title_text_color = self.props.selected_text_color
-            box.set_property(
-                'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-            box.update()
+            box.visible = True
 
         self.min_width = int(timeline_w + day_width_min)
-        self.min_height = int((24 + 1) * self._timeline.min_line_height)
+        self.min_height = int((24 + 1) * self._timeline.get_min_line_height(self))
 
-    def draw_week(self):
+    def _prepare_week_layout(self, w, h):
         """
-        Draws the currently selected week.
+        Prepares the layout for the currently selected week.
         """
-        x, y, w, h = self.get_bounds()
-        timeline_w = self._timeline.width
+        timeline_w = self._timeline.get_width(self)
         caption_size = max(len(day_name) for day_name in calendar.day_name)
-        caption_size += 3  # The needed space for the date before the day_name
+        caption_size += 3
         day_width_min = caption_size * self.font_size / Pango.SCALE
         day_width_max = (w - timeline_w) / 7
         self._day_width = max(day_width_min, day_width_max)
         self._day_height = h
 
-        # Redraw all days.
         cal = calendar.Calendar(self.firstweekday)
         weeks = util.my_monthdatescalendar(cal, self.selected_date)
         for weekno, week in enumerate(weeks):
-            # Hide all days that are not part of the current week.
             if self.selected_date not in week:
                 for dayno, date in enumerate(week):
                     box = self.days[weekno * 7 + dayno]
-                    box.set_property(
-                        'visibility', GooCanvas.CanvasItemVisibility.INVISIBLE)
+                    box.visible = False
                 continue
 
-            # Draw the days that are part of the current week.
             for dayno, current_date in enumerate(week):
-                # Highlight the day according to it's selection.
                 selected = current_date == self.selected_date
                 if selected:
                     the_border_color = self.props.selected_border_color
@@ -356,7 +345,6 @@ class Calendar(GooCanvas.Canvas):
                 else:
                     the_body_color = self.props.body_color
 
-                # Draw.
                 box = self.days[weekno * 7 + dayno]
                 box.x = self._day_width * dayno + timeline_w
                 box.y = 0
@@ -368,42 +356,30 @@ class Calendar(GooCanvas.Canvas):
                 box.border_color = the_border_color
                 box.body_color = the_body_color
                 box.title_text_color = the_text_color
-                box.set_property(
-                    'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-                box.update()
+                box.visible = True
 
                 if selected:
                     self._selected_day = box
-                    self._line_height = self._selected_day.line_height
 
         self.min_width = int(timeline_w + 7 * day_width_min)
-        self.min_height = int((24 + 1) * self._timeline.min_line_height)
+        self.min_height = int((24 + 1) * self._timeline.get_min_line_height(self))
 
-    def draw_month(self):
+    def _prepare_month_layout(self, w, h):
         """
-        Draws the currently selected month.
+        Prepares the layout for the currently selected month.
         """
-        x1, y1, w, h = self.get_bounds()
         caption_size = max(len(day_name) for day_name in calendar.day_name)
-        caption_size += 3  # The needed space for the date before the day_name
+        caption_size += 3
         day_width_min = caption_size * self.font_size / Pango.SCALE
         day_width_max = w / 7
         self._day_width = max(day_width_min, day_width_max)
         self._day_height = h / 6
 
-        # Hide the timeline.
-        if self._timeline is not None:
-            self._timeline.set_property(
-                'visibility', GooCanvas.CanvasItemVisibility.INVISIBLE)
-
-        # Draw the grid.
         y_pos = 0
         cal = calendar.Calendar(self.firstweekday)
         weeks = util.my_monthdatescalendar(cal, self.selected_date)
         for weekno, week in enumerate(weeks):
             for dayno, date in enumerate(week):
-                # The color depends on whether each day is part of the
-                # current month.
                 if (not util.same_month(date, self.selected_date)):
                     the_border_color = self.props.inactive_border_color
                     the_text_color = self.props.inactive_text_color
@@ -411,7 +387,6 @@ class Calendar(GooCanvas.Canvas):
                     the_border_color = self.props.border_color
                     the_text_color = self.props.text_color
 
-                # Highlight the day according to it's selection.
                 selected = date == self.selected_date
                 if selected:
                     the_border_color = self.props.selected_border_color
@@ -421,7 +396,6 @@ class Calendar(GooCanvas.Canvas):
                 else:
                     the_body_color = self.props.body_color
 
-                # Draw a box for the day.
                 box = self.days[weekno * 7 + dayno]
                 box.x = self._day_width * dayno
                 box.y = y_pos
@@ -433,18 +407,15 @@ class Calendar(GooCanvas.Canvas):
                 box.body_color = the_body_color
                 box.title_text_color = the_text_color
                 box.type = 'month'
-                box.set_property(
-                    'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-                box.update()
+                box.visible = True
 
                 if selected:
                     self._selected_day = box
-                    self._line_height = self._selected_day.line_height
 
             y_pos += self._day_height
 
         self.min_width = int(7 * day_width_min)
-        self.min_height = int((6 * 2 + 1) * self._timeline.min_line_height)
+        self.min_height = int((6 * 2 + 1) * self._timeline.get_min_line_height(self))
 
     def _get_day_item(self, find_date):
         cal = calendar.Calendar(self.firstweekday)
@@ -492,16 +463,18 @@ class Calendar(GooCanvas.Canvas):
                 return line
         return None
 
-    def draw_events(self):
-        _, _, bound_width, _ = self.get_bounds()
+    def _prepare_events(self):
+        alloc = self.get_allocation()
+        bound_width = alloc.width
+
         # Clear previous events.
-        for item in self._event_items:
-            item.remove()
         self._event_items = []
         for day in self.days:
             day.lines.clear()
             day.show_indic = False
-            day.update()
+            # Compute line height for each visible day
+            if day.visible:
+                day.compute_line_height(self)
 
         if not self._event_store:
             return
@@ -532,7 +505,6 @@ class Calendar(GooCanvas.Canvas):
         non_all_day_events = []
         for event in events:
             event.event_items = []
-            # Handle non-all-day events differently in week and day modes.
             if (self.view in {"week", "day"} and not event.all_day
                     and not event.multidays):
                 non_all_day_events.append(event)
@@ -547,13 +519,12 @@ class Calendar(GooCanvas.Canvas):
             if free_line is None:
                 for day in days:
                     day.show_indic = True
-                    day.update()
                 continue
 
             max_line_height = max(x.line_height for x in days)
             all_day_events_height = (free_line + 2) * max_line_height
-            all_day_events_height += (free_line + 1) * 2  # 2px margin per line
-            all_day_events_height += 1  # 1px padding-top
+            all_day_events_height += (free_line + 1) * 2
+            all_day_events_height += 1
             max_y = max(all_day_events_height, max_y)
             for day in days:
                 for i in range(free_line,
@@ -580,25 +551,18 @@ class Calendar(GooCanvas.Canvas):
                 if len(event.event_items):
                     event_item.no_caption = True
                 event.event_items.append(event_item)
-                event_item.connect('button_press_event',
-                    self.on_event_item_button_press_event)
-                event_item.connect('button_release_event',
-                    self.on_event_item_button_release)
-                event_item.connect('motion_notify_event',
-                    self.on_event_item_motion_notified)
                 self._event_items.append(event_item)
-                self.get_root_item().add_child(event_item, -1)
                 if self.view == "day":
-                    x_start = self._timeline.width
-                    width = bound_width - self._timeline.width
+                    x_start = self._timeline.get_width(self)
+                    width = bound_width - self._timeline.get_width(self)
                 else:
                     x_start = day.x
                     width = (day.width + 2) * len(week)
                 event_item.x = x_start
                 event_item.left_border = x_start + 2
                 event_item.y = day.y + (free_line + 1) * day.line_height
-                event_item.y += free_line * 2  # 2px of margin per line
-                event_item.y += 1  # 1px padding-top
+                event_item.y += free_line * 2
+                event_item.y += 1
                 event_item.width = width
                 event_item.height = day.line_height
                 week_start = week[0].date
@@ -619,32 +583,33 @@ class Calendar(GooCanvas.Canvas):
                     event_item.x += 2
                     event_item.width -= 6
                     event_item.type = 'leftright'
-                event_item.update()
+
         # Add the day title
         if self._selected_day:
             max_y += self._selected_day.line_height
+            self._line_height = self._selected_day.line_height
 
         if self.view == "month":
             return
 
-        # Redraw the timeline.
-        self._timeline.set_property(
-            'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-        x, y, w, h = self.get_bounds()
+        # Prepare the timeline.
+        alloc = self.get_allocation()
+        w, h = alloc.width, alloc.height
         max_y += (h - max_y) % 24
-        self._timeline.x = x
+        self._timeline.x = 0
         self._timeline.y = max_y
         self._timeline.height = h - max_y
         self._timeline.line_color = self.props.body_color
         self._timeline.bg_color = self.props.border_color
         self._timeline.text_color = self.props.text_color
-        self._timeline.update()
-        min_line_height = self._timeline.min_line_height
-        line_height = self._timeline.line_height
+        self._timeline.visible = True
+
+        min_line_height = self._timeline.get_min_line_height(self)
+        line_height = self._timeline.get_line_height(self)
         self.minute_height = line_height / 60.0
         self.min_height = int(max_y + 24 * min_line_height)
 
-        # Draw non-all-day events.
+        # Prepare non-all-day events.
         for date in dates:
             date_start = datetime.datetime.combine(date, datetime.time())
             date_end = (datetime.datetime.combine(date_start, datetime.time())
@@ -654,9 +619,7 @@ class Calendar(GooCanvas.Canvas):
                 date_start, date_end)
             day_events.sort()
             columns = []
-            column = 0
 
-            # Sort events into columns.
             remaining_events = day_events[:]
             while len(remaining_events) > 0:
                 columns.append([remaining_events[0]])
@@ -668,17 +631,14 @@ class Calendar(GooCanvas.Canvas):
                 for event in columns[-1]:
                     remaining_events.remove(event)
 
-            # Walk through all columns.
             for columnno, column in enumerate(columns):
                 for event in column:
-                    # Crop the event to the current day.
                     event1_start = max(event.start, date_start)
                     event1_end = min(event.end, date_end)
 
                     parallel = util.count_parallel_events(day_events,
                         event1_start, event1_end)
 
-                    # Draw.
                     top_offset = event1_start - date_start
                     bottom_offset = event1_end - event1_start
                     top_offset_mins = top_offset.seconds / 60
@@ -690,19 +650,12 @@ class Calendar(GooCanvas.Canvas):
                     if event.event_items:
                         event_item.no_caption = True
                     event.event_items.append(event_item)
-                    event_item.connect('button_press_event',
-                        self.on_event_item_button_press_event)
-                    event_item.connect('button_release_event',
-                        self.on_event_item_button_release)
-                    event_item.connect('motion_notify_event',
-                        self.on_event_item_motion_notified)
                     self._event_items.append(event_item)
-                    self.get_root_item().add_child(event_item, -1)
                     y_off1 = top_offset_mins * self.minute_height
                     y_off2 = bottom_offset_mins * self.minute_height
                     if self.view == "day":
-                        x_start = self._timeline.width
-                        column_width = (w - self._timeline.width) / parallel
+                        x_start = self._timeline.get_width(self)
+                        column_width = (w - self._timeline.get_width(self)) / parallel
                     else:
                         column_width = day.width / parallel
                         x_start = day.x
@@ -712,7 +665,7 @@ class Calendar(GooCanvas.Canvas):
                     event_item.width = column_width - 4
                     if columnno != (parallel - 1):
                         event_item.width += column_width / 1.2
-                    event_item.height = max(event_item.line_height, y_off2)
+                    event_item.height = max(event_item.get_line_height(self), y_off2)
                     if event.start < event1_start and event.end > event1_end:
                         event_item.type = 'mid'
                     elif event.start < event1_start:
@@ -721,7 +674,31 @@ class Calendar(GooCanvas.Canvas):
                         event_item.type = 'bottom'
                     else:
                         event_item.type = 'topbottom'
-                    event_item.update()
+
+    def on_draw(self, widget, cr):
+        """Handle the draw signal - draw the entire calendar."""
+        alloc = self.get_allocation()
+        w, h = alloc.width, alloc.height
+
+        # Draw background
+        cr.set_source_rgb(1, 1, 1)
+        cr.rectangle(0, 0, w, h)
+        cr.fill()
+
+        # Draw all visible day items
+        for day in self.days:
+            if day.visible:
+                day.draw(cr, self)
+
+        # Draw timeline for week/day views
+        if self.view in ("week", "day") and self._timeline.visible:
+            self._timeline.draw(cr, self)
+
+        # Draw all event items
+        for event_item in self._event_items:
+            event_item.draw(cr, self)
+
+        return False
 
     def on_event_store_event_removed(self, store, event):
         self.update()
@@ -743,81 +720,80 @@ class Calendar(GooCanvas.Canvas):
         elif event.keyval == Gdk.KEY_Right:
             self.select(date + datetime.timedelta(1))
 
-    @util.left_click
-    def on_day_item_button_press_event(self, day, widget2, event):
-        self.emit('day-pressed', day.date)
-        self.select(day.date)
+    def on_button_press_event(self, widget, event):
+        if event.button != 1:
+            return False
 
-        if self._is_double_click(event):
-            self.emit('day-activated', day.date)
+        x, y = event.x, event.y
 
-    def _is_double_click(self, event):
-        gtk_settings = Gtk.Settings.get_default()
-        double_click_distance = gtk_settings.props.gtk_double_click_distance
-        double_click_time = gtk_settings.props.gtk_double_click_time
-        if (self._last_click_x is not None
-                and event.time < (self._last_click_time + double_click_time)
-                and abs(event.x - self._last_click_x) <= double_click_distance
-                and abs(event.y - self._last_click_y) <= double_click_distance
-                ):
-            self._last_click_x = None
-            self._last_click_y = None
-            self._last_click_time = None
-            return True
-        else:
-            self._last_click_x = event.x
-            self._last_click_y = event.y
-            self._last_click_time = event.time
-            return False
+        # Check if an event item was clicked
+        for event_item in reversed(self._event_items):
+            if event_item.contains_point(x, y):
+                self._handle_event_item_press(event_item, event)
+                return True
 
-    def get_cur_pointed_date(self, x, y):
-        """
-        Return the date of the day_item pointed by two coordinates [x,y]
-        """
-        if self.view == 'day':
-            return self.selected_date
-        # Get current week
-        cal = calendar.Calendar(self.firstweekday)
-        weeks = util.my_monthdatescalendar(cal, self.selected_date)
-        if self.view == 'week':
-            cur_week, = (week for week in weeks for date in week
-                if self.selected_date == date)
-        elif self.view == 'month':
-            max_height = 6 * self._day_height
-            if y < 0:
-                weekno = 0
-            elif y > max_height:
-                weekno = 5
-            else:
-                weekno = int(y / self._day_height)
-            cur_week = weeks[weekno]
+        # Check if a day was clicked
+        for day in self.days:
+            if day.visible and day.contains_point(x, y):
+                self._handle_day_press(day, event)
+                return True
+
+        return False
+
+    def on_button_release_event(self, widget, event):
+        # Find if we're releasing on an event item
+        for event_item in self._event_items:
+            if event_item.contains_point(event.x, event.y):
+                if self._drag_x is not None:
+                    event_item.transparent = False
+                    self._stop_drag_and_drop()
+                    self.update()
+                    self.emit('event-released', event_item.event)
+                    return True
+        if self._drag_x is not None:
+            self._stop_drag_and_drop()
+            self.update()
+        return False
 
-        # Get Current pointed date
-        max_width = 7 * self._day_width
-        if x < 0:
-            day_no = 0
-        elif x > max_width:
-            day_no = 6
-        else:
-            offset_x = self._timeline.width if self.view == 'week' else 0
-            day_no = int((x - offset_x) / self._day_width)
-        return cur_week[day_no]
+    def on_motion_notify_event(self, widget, event):
+        if self._drag_x is None or self._drag_y is None:
+            return False
+
+        # Find the event item being dragged
+        for event_item in self._event_items:
+            if event_item.transparent:
+                self._handle_event_item_motion(event_item, event)
+                return True
+        return False
+
+    def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
+        # Check event items for tooltips
+        for event_item in reversed(self._event_items):
+            if event_item.contains_point(x, y):
+                tooltip_text = event_item.get_tooltip()
+                if tooltip_text:
+                    tooltip.set_text(tooltip_text)
+                    return True
+        return False
+
+    def _handle_day_press(self, day, event):
+        self.emit('day-pressed', day.date)
+        self.select(day.date)
 
-    @util.left_click
-    def on_event_item_button_press_event(self, event_item, rect, event):
+        if self._is_double_click(event):
+            self.emit('day-activated', day.date)
 
+    def _handle_event_item_press(self, event_item, event):
         if event_item.event.editable:
-            # Drag and drop starting coordinates
             self._drag_x = event.x
             self._drag_y = event.y
             self._drag_height = 0
             self._drag_start_date = self.get_cur_pointed_date(event.x, event.y)
             self._drag_date = self._drag_start_date
             self.set_has_tooltip(False)
-            event_item.raise_(None)
             event_item.transparent = True
 
-            event_item.width = self._day_width - 6  # Biggest event width
+            event_item.width = self._day_width - 6
             event_date = event_item.event.start.date()
             daysdelta = self._drag_start_date - event_date
             if self.view == 'week':
@@ -829,24 +805,22 @@ class Calendar(GooCanvas.Canvas):
                     if event_item.event.end:
                         event_item.event.end += daysdelta
                 else:
+                    # Remove other event items for this event
                     for item in event_item.event.event_items:
                         if item != event_item:
-                            item.remove()
                             self._event_items.remove(item)
 
                     event_item.height = 2 * self._line_height
-                    day_no = (int((event.x - self._timeline.width)
+                    day_no = (int((event.x - self._timeline.get_width(self))
                         / self._day_width))
                     day_off = day_no * self._day_width + 2
-                    event_item.x = self._timeline.width + day_off
+                    event_item.x = self._timeline.get_width(self) + day_off
                     if (event_item.no_caption or event.y < event_item.y
                             or event.y > (event_item.y + event_item.height)):
-                        # click was not performed inside the new day item
                         level_height = self.minute_height * self.MIN_PER_LEVEL
                         cur_level = int((event.y - self._timeline.y)
                             / level_height)
                         nb_levels_per_hour = 60 / self.MIN_PER_LEVEL
-                        # click is in the middle
                         cur_level -= nb_levels_per_hour
                         if cur_level < 0:
                             cur_level = 0
@@ -866,7 +840,6 @@ class Calendar(GooCanvas.Canvas):
             elif self.view == 'month':
                 for item in event_item.event.event_items:
                     if item != event_item:
-                        item.remove()
                         self._event_items.remove(item)
                     else:
                         event_item.event.start += daysdelta
@@ -876,27 +849,141 @@ class Calendar(GooCanvas.Canvas):
                         day_no = int(event.x / self._day_width)
                         event_item.y = weekno * self._day_height
                         event_item.y += (
-                            int(self._line_height) + 1)  # padding-top
+                            int(self._line_height) + 1)
                         event_item.x = (
-                            day_no * self._day_width + 2)  # padding-left
+                            day_no * self._day_width + 2)
                         item_height = (
-                            self._line_height + 2)  # 2px between items
+                            self._line_height + 2)
                         while event_item.y < event.y:
                             event_item.y += item_height
                         event_item.y -= item_height
                         event_item.no_caption = False
-            event_item.update()
+            self.queue_draw()
+
         self.emit('event-pressed', event_item.event)
 
         if self._is_double_click(event):
             self._stop_drag_and_drop()
             self.emit('event-activated', event_item.event)
 
-    def on_event_item_button_release(self, event_item, rect, event):
-        event_item.transparent = False
-        self._stop_drag_and_drop()
-        self.draw_events()
-        self.emit('event-released', event_item.event)
+    def _handle_event_item_motion(self, event_item, event):
+        diff_y = event.y - self._drag_y
+        self._drag_x = event.x
+        self._drag_y = event.y
+        self._drag_height += diff_y
+
+        cur_pointed_date = self.get_cur_pointed_date(event.x, event.y)
+        daysdelta = cur_pointed_date - self._drag_date
+        if self.view == 'month':
+            if cur_pointed_date != self._drag_date:
+                event_item.event.start += daysdelta
+                if event_item.event.end:
+                    event_item.event.end += daysdelta
+                nb_lines = int(round(float(daysdelta.days) / 7))
+                nb_columns = daysdelta.days - nb_lines * 7
+                event_item.x += nb_columns * self._day_width
+                self._drag_date = cur_pointed_date
+            event_item.y += diff_y
+            self.queue_draw()
+            return
+
+        # Handle horizontal translation
+        if cur_pointed_date != self._drag_date:
+            self._drag_date = cur_pointed_date
+            event_item.event.start += daysdelta
+            if event_item.event.end:
+                event_item.event.end += daysdelta
+            event_item.x += daysdelta.days * self._day_width
+
+        if event_item.event.multidays or event_item.event.all_day:
+            self.queue_draw()
+            return
+
+        # Compute vertical translation
+        diff_minutes = int(round(self._drag_height / self.minute_height))
+        diff_time = datetime.timedelta(minutes=diff_minutes)
+        old_start = event_item.event.start
+        new_start = old_start + diff_time
+        next_level = util.next_level(old_start, self.MIN_PER_LEVEL)
+        prev_level = util.prev_level(old_start, self.MIN_PER_LEVEL)
+        if diff_time >= datetime.timedelta(0) and new_start >= next_level:
+            new_start = util.prev_level(new_start, self.MIN_PER_LEVEL)
+        elif diff_time < datetime.timedelta(0) and new_start <= prev_level:
+            new_start = util.next_level(new_start, self.MIN_PER_LEVEL)
+        else:
+            self.queue_draw()
+            return
+
+        # Apply vertical translation
+        midnight = datetime.time()
+        old_start_midnight = datetime.datetime.combine(old_start, midnight)
+        onedaydelta = datetime.timedelta(days=1)
+        next_day_midnight = old_start_midnight + onedaydelta
+        if new_start.day < old_start.day:
+            new_start = old_start_midnight
+        elif new_start >= next_day_midnight:
+            seconds_per_level = 60 * self.MIN_PER_LEVEL
+            level_delta = datetime.timedelta(seconds=seconds_per_level)
+            last_level = next_day_midnight - level_delta
+            new_start = last_level
+        event_item.event.start = new_start
+        if event_item.event.end:
+            timedelta = new_start - old_start
+            event_item.event.end += timedelta
+        pxdelta = (timedelta.total_seconds() / 60 * self.minute_height)
+        event_item.y += pxdelta
+        self.queue_draw()
+        self._drag_height -= pxdelta
+
+    def _is_double_click(self, event):
+        gtk_settings = Gtk.Settings.get_default()
+        double_click_distance = gtk_settings.props.gtk_double_click_distance
+        double_click_time = gtk_settings.props.gtk_double_click_time
+        if (self._last_click_x is not None
+                and event.time < (self._last_click_time + double_click_time)
+                and abs(event.x - self._last_click_x) <= double_click_distance
+                and abs(event.y - self._last_click_y) <= double_click_distance
+                ):
+            self._last_click_x = None
+            self._last_click_y = None
+            self._last_click_time = None
+            return True
+        else:
+            self._last_click_x = event.x
+            self._last_click_y = event.y
+            self._last_click_time = event.time
+            return False
+
+    def get_cur_pointed_date(self, x, y):
+        """
+        Return the date of the day_item pointed by two coordinates [x,y]
+        """
+        if self.view == 'day':
+            return self.selected_date
+        cal = calendar.Calendar(self.firstweekday)
+        weeks = util.my_monthdatescalendar(cal, self.selected_date)
+        if self.view == 'week':
+            cur_week, = (week for week in weeks for date in week
+                if self.selected_date == date)
+        elif self.view == 'month':
+            max_height = 6 * self._day_height
+            if y < 0:
+                weekno = 0
+            elif y > max_height:
+                weekno = 5
+            else:
+                weekno = int(y / self._day_height)
+            cur_week = weeks[weekno]
+
+        max_width = 7 * self._day_width
+        if x < 0:
+            day_no = 0
+        elif x > max_width:
+            day_no = 6
+        else:
+            offset_x = self._timeline.get_width(self) if self.view == 'week' else 0
+            day_no = int((x - offset_x) / self._day_width)
+        return cur_week[day_no]
 
     def _stop_drag_and_drop(self):
         self._drag_x = None
@@ -906,77 +993,13 @@ class Calendar(GooCanvas.Canvas):
         self._drag_date = None
         self.set_has_tooltip(True)
 
-    def on_event_item_motion_notified(self, event_item, rect, event):
-        if self._drag_x and self._drag_y:
-            # We are currently drag and dropping this event item
-            diff_y = event.y - self._drag_y
-            self._drag_x = event.x
-            self._drag_y = event.y
-            self._drag_height += diff_y
+    def get_root_item(self):
+        # Compatibility method for GooCanvas API
+        return self
 
-            cur_pointed_date = self.get_cur_pointed_date(event.x, event.y)
-            daysdelta = cur_pointed_date - self._drag_date
-            if self.view == 'month':
-                if cur_pointed_date != self._drag_date:
-                    event_item.event.start += daysdelta
-                    if event_item.event.end:
-                        event_item.event.end += daysdelta
-                    nb_lines = int(round(float(daysdelta.days) / 7))
-                    nb_columns = daysdelta.days - nb_lines * 7
-                    event_item.x += nb_columns * self._day_width
-                    self._drag_date = cur_pointed_date
-                event_item.y += diff_y
-                event_item.update()
-                return
-
-            # Handle horizontal translation
-            if cur_pointed_date != self._drag_date:
-                self._drag_date = cur_pointed_date
-                event_item.event.start += daysdelta
-                if event_item.event.end:
-                    event_item.event.end += daysdelta
-                event_item.x += daysdelta.days * self._day_width
-
-            if event_item.event.multidays or event_item.event.all_day:
-                event_item.update()
-                return
-
-            # Compute vertical translation
-            diff_minutes = int(round(self._drag_height / self.minute_height))
-            diff_time = datetime.timedelta(minutes=diff_minutes)
-            old_start = event_item.event.start
-            new_start = old_start + diff_time
-            next_level = util.next_level(old_start, self.MIN_PER_LEVEL)
-            prev_level = util.prev_level(old_start, self.MIN_PER_LEVEL)
-            if diff_time >= datetime.timedelta(0) and new_start >= next_level:
-                new_start = util.prev_level(new_start, self.MIN_PER_LEVEL)
-            elif diff_time < datetime.timedelta(0) and new_start <= prev_level:
-                new_start = util.next_level(new_start, self.MIN_PER_LEVEL)
-            else:
-                # We stay at the same level
-                event_item.update()
-                return
-
-            # Apply vertical translation
-            midnight = datetime.time()
-            old_start_midnight = datetime.datetime.combine(old_start, midnight)
-            onedaydelta = datetime.timedelta(days=1)
-            next_day_midnight = old_start_midnight + onedaydelta
-            if new_start.day < old_start.day:
-                new_start = old_start_midnight
-            elif new_start >= next_day_midnight:
-                seconds_per_level = 60 * self.MIN_PER_LEVEL
-                level_delta = datetime.timedelta(seconds=seconds_per_level)
-                last_level = next_day_midnight - level_delta
-                new_start = last_level
-            event_item.event.start = new_start
-            if event_item.event.end:
-                timedelta = new_start - old_start
-                event_item.event.end += timedelta
-            pxdelta = (timedelta.total_seconds() / 60 * self.minute_height)
-            event_item.y += pxdelta
-            event_item.update()
-            self._drag_height -= pxdelta
+    def grab_focus(self, item=None):
+        # Compatibility method for GooCanvas API
+        super().grab_focus()
 
 
 GObject.signal_new('event-pressed',
@@ -1021,14 +1044,20 @@ GObject.signal_new('page-changed',
     (GObject.TYPE_PYOBJECT,))
 
 
-class DayItem(GooCanvas.CanvasGroup):
+def parse_color(color_string):
+    """Parse a color string to RGB values (0-1 range)."""
+    color = Gdk.RGBA()
+    if color.parse(color_string):
+        return color.red, color.green, color.blue, color.alpha
+    return 0, 0, 0, 1
+
+
+class DayItem:
     """
-    A canvas item representing a day.
+    A data structure representing a day.
     """
 
     def __init__(self, cal, **kwargs):
-        super(DayItem, self).__init__()
-
         self._cal = cal
         self.x = kwargs.get('x', 0)
         self.y = kwargs.get('y', 0)
@@ -1044,41 +1073,72 @@ class DayItem(GooCanvas.CanvasGroup):
         self.n_lines = 0
         self.title_text_color = ""
         self.line_height = 0
+        self.visible = True
 
-        # Create canvas items.
-        self.border = GooCanvas.CanvasRect(parent=self)
-        self.text = GooCanvas.CanvasText(parent=self)
-        self.box = GooCanvas.CanvasRect(parent=self)
-        self.indic = GooCanvas.CanvasRect(parent=self)
+    def contains_point(self, px, py):
+        """Check if a point is within this day item."""
+        return (self.x <= px <= self.x + self.width and
+                self.y <= py <= self.y + self.height)
 
-    def update(self):
+    def compute_line_height(self, cal):
+        """Compute the line height based on font."""
         if not self.date:
             return
 
         week_day = self.date.weekday()
         day_name = calendar.day_name[week_day]
         caption = '%s %s' % (self.date.day, day_name)
-        self.text.set_property('font', self._cal.props.font)
-        self.text.set_property('text', caption)
-        logical_height = self.text.get_natural_extents()[1].height
-        line_height = int(math.ceil(float(logical_height) / Pango.SCALE))
+
+        # Create a Pango layout to measure text
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+        layout.set_text(caption, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        self.line_height = logical_rect.height
+
+        # Compute number of lines that fit
+        if self.full_border:
+            box_height = max(self.height - self.line_height - 3, 0)
+        else:
+            box_height = max(self.height - self.line_height, 0)
+        line_height_and_margin = self.line_height + 2
+        self.n_lines = int(box_height / line_height_and_margin) if line_height_and_margin > 0 else 0
+
+    def draw(self, cr, cal):
+        """Draw this day item using Cairo."""
+        if not self.date:
+            return
+
+        week_day = self.date.weekday()
+        day_name = calendar.day_name[week_day]
+        caption = '%s %s' % (self.date.day, day_name)
+
+        # Create a Pango layout
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+        layout.set_text(caption, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        line_height = logical_rect.height
         self.line_height = line_height
 
-        # Draw the border.
-        self.border.set_property('x', self.x)
-        self.border.set_property('y', self.y)
-        self.border.set_property('width', self.width)
-        self.border.set_property('height', self.height)
-        self.border.set_property('stroke_color', self.border_color)
-        self.border.set_property('fill_color', self.border_color)
+        # Draw the border (filled rectangle)
+        r, g, b, a = parse_color(self.border_color)
+        cr.set_source_rgba(r, g, b, a)
+        cr.rectangle(self.x, self.y, self.width, self.height)
+        cr.fill()
 
-        # Draw the title text.
+        # Draw the title text
+        r, g, b, a = parse_color(self.title_text_color)
+        cr.set_source_rgba(r, g, b, a)
         padding_left = 2
-        self.text.set_property('x', self.x + padding_left)
-        self.text.set_property('y', self.y)
-        self.text.set_property('fill_color', self.title_text_color)
+        cr.move_to(self.x + padding_left, self.y)
+        PangoCairo.show_layout(cr, layout)
 
-        # Print the "body" of the day.
+        # Draw the "body" of the day
         if self.full_border:
             box_x = self.x + 2
             box_y = self.y + line_height
@@ -1089,56 +1149,45 @@ class DayItem(GooCanvas.CanvasGroup):
             box_y = self.y + line_height
             box_width = max(self.width - 2, 0)
             box_height = max(self.height - line_height, 0)
-        self.box.set_property('x', box_x)
-        self.box.set_property('y', box_y)
-        self.box.set_property('width', box_width)
-        self.box.set_property('height', box_height)
-        self.box.set_property('stroke_color', self.body_color)
-        self.box.set_property('fill_color', self.body_color)
-
-        line_height_and_margin = line_height + 2  # 2px of margin per line
-        self.n_lines = int(box_height / line_height_and_margin)
-
-        # Show an indicator in the title, if requested.
-        if not self.show_indic:
-            self.indic.set_property(
-                'visibility', GooCanvas.CanvasItemVisibility.INVISIBLE)
-            return
 
-        self.indic.set_property(
-            'visibility', GooCanvas.CanvasItemVisibility.VISIBLE)
-        self.indic.set_property('x',
-            self.x + self.width - line_height / 1.5)
-        self.indic.set_property('y', self.y + line_height / 3)
-        self.indic.set_property('width', line_height / 3)
-        self.indic.set_property('height', line_height / 3)
-        self.indic.set_property('stroke_color', self.title_text_color)
-        self.indic.set_property('fill_color', self.title_text_color)
-
-        # Draw a triangle.
-        x1 = self.x + self.width - line_height / 1.5
-        y1 = self.y + line_height / 3
-        x2 = x1 + line_height / 6
-        y2 = y1 + line_height / 3
-        x3 = x1 + line_height / 3
-        y3 = y1
-        path = 'M%s,%s L%s,%s L%s,%s Z' % (x1, y1, x2, y2, x3, y3)
-        self.indic.set_property('clip_path', path)
-
-
-class EventItem(GooCanvas.CanvasGroup):
+        r, g, b, a = parse_color(self.body_color)
+        cr.set_source_rgba(r, g, b, a)
+        cr.rectangle(box_x, box_y, box_width, box_height)
+        cr.fill()
+
+        line_height_and_margin = line_height + 2
+        self.n_lines = int(box_height / line_height_and_margin) if line_height_and_margin > 0 else 0
+
+        # Draw indicator if needed
+        if self.show_indic:
+            r, g, b, a = parse_color(self.title_text_color)
+            cr.set_source_rgba(r, g, b, a)
+            # Draw a triangle indicator
+            x1 = self.x + self.width - line_height / 1.5
+            y1 = self.y + line_height / 3
+            x2 = x1 + line_height / 6
+            y2 = y1 + line_height / 3
+            x3 = x1 + line_height / 3
+            y3 = y1
+            cr.move_to(x1, y1)
+            cr.line_to(x2, y2)
+            cr.line_to(x3, y3)
+            cr.close_path()
+            cr.fill()
+
+
+class EventItem:
     """
-    A canvas item representing an event.
+    A data structure representing an event.
     """
 
     def __init__(self, cal, **kwargs):
-        super(EventItem, self).__init__()
-
         self._cal = cal
-        self.x = kwargs.get('x')
-        self.y = kwargs.get('y')
-        self.width = kwargs.get('width')
-        self.height = kwargs.get('height')
+        self.x = kwargs.get('x', 0)
+        self.y = kwargs.get('y', 0)
+        self.width = kwargs.get('width', 0)
+        self.height = kwargs.get('height', 0)
+        self.left_border = 0
         self.bg_color = kwargs.get('bg_color')
         self.text_color = kwargs.get('text_color', 'black')
         self.event = kwargs.get('event')
@@ -1146,239 +1195,275 @@ class EventItem(GooCanvas.CanvasGroup):
         self.time_format = kwargs.get('time_format')
         self.transparent = False
         self.no_caption = False
+        self._line_height = None
+
+    def get_line_height(self, cal):
+        """Get the line height for this event item."""
+        if self._line_height is not None:
+            return self._line_height
+
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+        layout.set_text("Test", -1)
+        _, logical_rect = layout.get_pixel_extents()
+        self._line_height = logical_rect.height
+        return self._line_height
+
+    def contains_point(self, px, py):
+        """Check if a point is within this event item."""
+        return (self.x <= px <= self.x + self.width and
+                self.y <= py <= self.y + self.height)
+
+    def get_tooltip(self):
+        """Get the tooltip text for this event."""
+        startdate = self.event.start.strftime('%x')
+        starttime = self.event.start.strftime(self.time_format)
+        if self.event.end:
+            enddate = self.event.end.strftime('%x')
+            endtime = self.event.end.strftime(self.time_format)
 
-        # Create canvas items.
-        self.box = GooCanvas.CanvasRect(parent=self)
-        self.text = GooCanvas.CanvasText(parent=self)
-        self.text.set_property('font', self._cal.props.font)
-        logical_height = self.text.get_natural_extents()[1].height
-        self.line_height = logical_height / Pango.SCALE
-
-        if self.x is not None:
-            self.update()
+        if self.event.all_day:
+            if not self.event.end:
+                return '%s\n%s' % (startdate, self.event.caption)
+            else:
+                return '%s - %s\n%s' % (startdate, enddate, self.event.caption)
+        elif self.event.multidays:
+            if not self.event.end:
+                return '%s %s\n%s' % (startdate, starttime, self.event.caption)
+            else:
+                return '%s %s - %s %s\n%s' % (startdate, starttime,
+                    enddate, endtime, self.event.caption)
+        else:
+            if not self.event.end:
+                return '%s\n%s' % (starttime, self.event.caption)
+            else:
+                return '%s - %s\n%s' % (starttime, endtime, self.event.caption)
 
-    def update(self):
-        if (self.event.all_day or self._cal.view == "month"
+    def draw(self, cr, cal):
+        """Draw this event item using Cairo."""
+        if (self.event.all_day or cal.view == "month"
                 or self.event.multidays):
-            self.update_all_day_event()
+            self._draw_all_day_event(cr, cal)
         else:
-            self.update_event()
+            self._draw_event(cr, cal)
 
-    def update_event(self):
+    def _draw_event(self, cr, cal):
         self.width = max(self.width, 0)
         starttime = self.event.start.strftime(self.time_format)
         endtime = self.event.end.strftime(self.time_format)
-        tooltip = '%s - %s\n%s' % (starttime, endtime, self.event.caption)
 
-        # Do we have enough width for caption
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+
         first_line = starttime + ' - ' + endtime
-        self.text.set_property('text', first_line)
-        logical_width = self.text.get_natural_extents()[1].width / Pango.SCALE
-        if self.width < logical_width:
+        layout.set_text(first_line, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        line_height = logical_rect.height
+        self._line_height = line_height
+
+        if self.width < logical_rect.width:
             first_line = starttime + ' - '
 
         second_line = self.event.caption
-        self.text.set_property('text', second_line)
-        logical_width = self.text.get_natural_extents()[1].width / Pango.SCALE
-        if self.width < logical_width:
+        layout.set_text(second_line, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        if self.width < logical_rect.width:
             second_line = None
 
-        # Do we have enough height for whole caption
-        if self.height >= (2 * self.line_height):
+        if self.height >= (2 * line_height):
             caption = first_line
             if second_line:
                 caption += '\n' + second_line
-        elif self.height >= self.line_height:
+        elif self.height >= line_height:
             caption = first_line
         else:
             caption = ''
         caption = '' if self.no_caption else caption
-        the_event_bg_color = self.event.bg_color
 
-        # Choose text color.
+        the_event_bg_color = self.event.bg_color
         if self.event.text_color is None:
             the_event_text_color = self.text_color
         else:
             the_event_text_color = self.event.text_color
 
         if the_event_bg_color is not None:
-            self.box.set_property('x', self.x)
-            self.box.set_property('y', self.y)
-            self.box.set_property('width', self.width)
-            self.box.set_property('height', self.height)
-            self.box.set_property('stroke_color', the_event_bg_color)
-            self.box.set_property('fill_color', the_event_bg_color)
-            # Alpha color is set to half of 255, i.e an opacity of 5O percents
-            transparent_color = self.box.get_property('fill_color_rgba') - 128
+            r, g, b, a = parse_color(the_event_bg_color)
             if self.transparent:
-                self.box.set_property('stroke_color_rgba', transparent_color)
-                self.box.set_property('fill_color_rgba', transparent_color)
-            self.box.set_property('tooltip', tooltip)
-
-        # Print the event name into the title box.
-        self.text.set_property('x', self.x + 2)
-        self.text.set_property('y', self.y)
-        self.text.set_property('text', caption)
-        self.text.set_property('fill_color', the_event_text_color)
-        self.text.set_property('tooltip', tooltip)
-
-        # Clip the text.
-        x2, y2 = self.x + self.width, self.y + self.height,
-        path = 'M%s,%s L%s,%s L%s,%s L%s,%s Z' % (self.x, self.y, self.x, y2,
-            x2, y2, x2, self.y)
-        self.text.set_property('clip_path', path)
-
-    def update_all_day_event(self):
+                a = 0.5
+            cr.set_source_rgba(r, g, b, a)
+            cr.rectangle(self.x, self.y, self.width, self.height)
+            cr.fill()
+
+        # Draw the text
+        r, g, b, a = parse_color(the_event_text_color)
+        cr.set_source_rgba(r, g, b, a)
+
+        # Clip to event bounds
+        cr.save()
+        cr.rectangle(self.x, self.y, self.width, self.height)
+        cr.clip()
+
+        layout.set_text(caption, -1)
+        cr.move_to(self.x + 2, self.y)
+        PangoCairo.show_layout(cr, layout)
+
+        cr.restore()
+
+    def _draw_all_day_event(self, cr, cal):
         self.width = max(self.width, 0)
         startdate = self.event.start.strftime('%x')
         starttime = self.event.start.strftime(self.time_format)
-        if self.event.end:
-            enddate = self.event.end.strftime('%x')
-            endtime = self.event.end.strftime(self.time_format)
-        caption = self.event.caption
 
-        if self.event.all_day:
-            if not self.event.end:
-                tooltip = '%s\n%s' % (startdate, caption)
-            else:
-                tooltip = '%s - %s\n%s' % (startdate, enddate, caption)
-        elif self.event.multidays:
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
+
+        caption = self.event.caption
+        if not self.event.all_day and not self.event.multidays:
             caption = '%s %s' % (starttime, caption)
-            if not self.event.end:
-                tooltip = '%s %s\n%s' % (startdate, starttime, caption)
-            else:
-                tooltip = '%s %s - %s %s\n%s' % (startdate, starttime,
-                    enddate, endtime, caption)
-        else:
+        elif self.event.multidays:
             caption = '%s %s' % (starttime, caption)
-            if not self.event.end:
-                tooltip = '%s\n%s' % (starttime, caption)
-            else:
-                tooltip = '%s - %s\n%s' % (starttime, endtime, caption)
+
         caption = '' if self.no_caption else caption
-        the_event_bg_color = self.event.bg_color
-        self.text.set_property('text', caption)
-        logical_height = self.text.get_natural_extents()[1].height
-        self.height = logical_height / Pango.SCALE
+        layout.set_text(caption, -1)
+        _, logical_rect = layout.get_pixel_extents()
+        self.height = logical_rect.height
+        self._line_height = logical_rect.height
 
-        # Choose text color.
+        the_event_bg_color = self.event.bg_color
         if self.event.text_color is None:
             the_event_text_color = self.text_color
         else:
             the_event_text_color = self.event.text_color
 
         if the_event_bg_color is not None:
-            self.box.set_property('x', self.x)
-            self.box.set_property('y', self.y)
-            self.box.set_property('width', self.width)
-            self.box.set_property('height', self.height)
-            self.box.set_property('stroke_color', the_event_bg_color)
-            self.box.set_property('fill_color', the_event_bg_color)
-            transparent_color = self.box.get_property('fill_color_rgba') - 128
+            r, g, b, a = parse_color(the_event_bg_color)
             if self.transparent:
-                self.box.set_property('stroke_color_rgba', transparent_color)
-                self.box.set_property('fill_color_rgba', transparent_color)
-            self.box.set_property('tooltip', tooltip)
+                a = 0.5
+            cr.set_source_rgba(r, g, b, a)
+            cr.rectangle(self.x, self.y, self.width, self.height)
+            cr.fill()
 
-        # Print the event name into the title box.
-        self.text.set_property('x', self.x + 2)
-        self.text.set_property('y', self.y)
-        self.text.set_property('fill_color', the_event_text_color)
-        self.text.set_property('tooltip', tooltip)
+        # Draw the text
+        r, g, b, a = parse_color(the_event_text_color)
+        cr.set_source_rgba(r, g, b, a)
 
-        # Clip the text.
-        x2, y2 = self.x + self.width, self.y + self.height,
-        path = 'M%s,%s L%s,%s L%s,%s L%s,%s Z' % (
-            self.x, self.y, self.x, y2, x2, y2, x2, self.y)
-        self.text.set_property('clip_path', path)
+        # Clip to event bounds
+        cr.save()
+        cr.rectangle(self.x, self.y, self.width, self.height)
+        cr.clip()
 
+        cr.move_to(self.x + 2, self.y)
+        PangoCairo.show_layout(cr, layout)
 
-class TimelineItem(GooCanvas.CanvasGroup):
+        cr.restore()
+
+
+class TimelineItem:
     """
-    A canvas item representing a timeline.
+    A data structure representing a timeline.
     """
 
     def __init__(self, cal, **kwargs):
-        super(TimelineItem, self).__init__()
-
         self._cal = cal
-        self.x = kwargs.get('x')
-        self.y = kwargs.get('y')
+        self.x = kwargs.get('x', 0)
+        self.y = kwargs.get('y', 0)
+        self.height = kwargs.get('height', 0)
         self.line_color = kwargs.get('line_color')
         self.bg_color = kwargs.get('bg_color')
         self.text_color = kwargs.get('text_color')
         self.time_format = kwargs.get('time_format')
-        self.width = 0
+        self._width = None
+        self._min_line_height = None
+        self.visible = False
+
+    def get_width(self, cal):
+        """Compute the width needed for the timeline."""
+        if self._width is not None:
+            return self._width
+
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
 
-        # Create canvas items.
-        self._timeline_rect = {}
-        self._timeline_text = {}
+        max_width = 0
         for n in range(24):
             caption = datetime.time(n).strftime(self.time_format)
-            self._timeline_rect[n] = GooCanvas.CanvasRect(parent=self)
-            self._timeline_text[n] = GooCanvas.CanvasText(
-                parent=self, text=caption)
+            layout.set_text(caption, -1)
+            ink_rect, logical_rect = layout.get_pixel_extents()
+            max_width = max(max_width, logical_rect.width)
 
-        if self.x is not None:
-            self.update()
-        else:
-            self._compute_width()
+        self._width = max_width + 4  # Add some padding
+        return self._width
 
-    @property
-    def min_line_height(self):
-        logical_height = 0
-        self.ink_padding_top = 0
-        for n in range(24):
-            natural_extents = self._timeline_text[n].get_natural_extents()
-            logical_rect = natural_extents[1]
-            logical_height = max(logical_height, logical_rect.height)
-            ink_rect = natural_extents[0]
-            self.ink_padding_top = max(self.ink_padding_top, ink_rect.x)
-        line_height = int(math.ceil(float(logical_height) / Pango.SCALE))
-        return line_height
+    def get_min_line_height(self, cal):
+        """Get the minimum line height for the timeline."""
+        if self._min_line_height is not None:
+            return self._min_line_height
 
-    @property
-    def line_height(self):
-        self.padding_top = 0
-        line_height = self.min_line_height
-        if line_height < self.height // 24:
-            line_height = self.height // 24
-            padding_top = (line_height - self._cal.font_size / Pango.SCALE) / 2
-            padding_top -= int(math.ceil(
-                    float(self.ink_padding_top) / Pango.SCALE))
-            self.padding_top = padding_top
-        return line_height
-
-    def _compute_width(self):
-        font = self._cal.props.font
-        ink_padding_left = 0
-        ink_max_width = 0
-        for n in range(24):
-            self._timeline_text[n].set_property('font', font)
-            natural_extents = self._timeline_text[n].get_natural_extents()
-            ink_rect = natural_extents[0]
-            ink_padding_left = max(ink_padding_left, ink_rect.x)
-            ink_max_width = max(ink_max_width, ink_rect.width)
-        self.width = int(math.ceil(
-                float(ink_padding_left + ink_max_width) / Pango.SCALE))
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
 
-    def update(self):
-        self._compute_width()
-        line_height = self.line_height
+        max_height = 0
+        for n in range(24):
+            caption = datetime.time(n).strftime(self.time_format)
+            layout.set_text(caption, -1)
+            _, logical_rect = layout.get_pixel_extents()
+            max_height = max(max_height, logical_rect.height)
+
+        self._min_line_height = max_height
+        return self._min_line_height
+
+    def get_line_height(self, cal):
+        """Get the actual line height for drawing."""
+        min_height = self.get_min_line_height(cal)
+        if self.height > 0 and min_height < self.height // 24:
+            return self.height // 24
+        return min_height
+
+    def draw(self, cr, cal):
+        """Draw the timeline using Cairo."""
+        width = self.get_width(cal)
+        line_height = self.get_line_height(cal)
+        min_line_height = self.get_min_line_height(cal)
+
+        # Compute padding for centering text
+        padding_top = 0
+        if line_height > min_line_height:
+            padding_top = (line_height - min_line_height) / 2
+
+        pango_context = cal.get_pango_context()
+        layout = Pango.Layout(pango_context)
+        layout.set_font_description(
+            Pango.FontDescription.from_string(cal.props.font))
 
-        # Draw the timeline.
         for n in range(24):
-            rect = self._timeline_rect[n]
-            text = self._timeline_text[n]
             y = self.y + n * line_height
+            caption = datetime.time(n).strftime(self.time_format)
 
-            rect.set_property('x', self.x)
-            rect.set_property('y', y)
-            rect.set_property('width', self.width)
-            rect.set_property('height', line_height)
-            rect.set_property('stroke_color', self.line_color)
-            rect.set_property('fill_color', self.bg_color)
-
-            text.set_property('x', self.x)
-            text.set_property('y', y + self.padding_top)
-            text.set_property('fill_color', self.text_color)
+            # Draw background
+            r, g, b, a = parse_color(self.bg_color)
+            cr.set_source_rgba(r, g, b, a)
+            cr.rectangle(self.x, y, width, line_height)
+            cr.fill()
+
+            # Draw line separator
+            r, g, b, a = parse_color(self.line_color)
+            cr.set_source_rgba(r, g, b, a)
+            cr.rectangle(self.x, y, width, 1)
+            cr.fill()
+
+            # Draw text
+            r, g, b, a = parse_color(self.text_color)
+            cr.set_source_rgba(r, g, b, a)
+            layout.set_text(caption, -1)
+            cr.move_to(self.x, y + padding_top)
+            PangoCairo.show_layout(cr, layout)
--- a/setup.py
+++ b/setup.py
@@ -23,7 +23,7 @@ def get_version():
 
 setup(name='GooCalendar',
     version=get_version(),
-    description='A calendar widget for GTK using PyGoocanvas',
+    description='A calendar widget for GTK',
     long_description=read('README'),
     author='Tryton',
     author_email='[email protected]',
@@ -35,7 +35,7 @@ setup(name='GooCalendar',
         "Forum": 'https://discuss.tryton.org/tags/goocalendar',
         "Source Code": 'https://code.tryton.org/goocalendar',
         },
-    keywords='calendar GTK GooCanvas widget',
+    keywords='calendar GTK widget',
     packages=find_packages(),
     classifiers=[
         'Development Status :: 5 - Production/Stable',

Reply via email to