[hamster-applet] how did i forget these?
- From: Toms Baugis <tbaugis src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [hamster-applet] how did i forget these?
- Date: Thu, 19 Nov 2009 18:12:08 +0000 (UTC)
commit 9239c37c622afd181e5ec1b938e51b876d234a14
Author: Toms Bauģis <toms baugis gmail com>
Date: Thu Nov 19 18:11:32 2009 +0000
how did i forget these?
hamster/widgets/dayline.py | 346 ++++++++++++++++++++++++++++++++
hamster/widgets/reportchooserdialog.py | 167 +++++++++++++++
hamster/widgets/timeline.py | 174 ++++++++++++++++
3 files changed, 687 insertions(+), 0 deletions(-)
---
diff --git a/hamster/widgets/dayline.py b/hamster/widgets/dayline.py
new file mode 100644
index 0000000..15b90ee
--- /dev/null
+++ b/hamster/widgets/dayline.py
@@ -0,0 +1,346 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2007-2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with Project Hamster. If not, see <http://www.gnu.org/licenses/>.
+
+import gtk
+import gobject
+
+from hamster import stuff
+from hamster import graphics
+
+import datetime as dt
+import colorsys
+
+
+class DayLine(graphics.Area):
+ def __init__(self):
+ graphics.Area.__init__(self)
+
+ self.set_events(gtk.gdk.EXPOSURE_MASK
+ | gtk.gdk.LEAVE_NOTIFY_MASK
+ | gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.POINTER_MOTION_MASK
+ | gtk.gdk.POINTER_MOTION_HINT_MASK)
+ self.connect("button_release_event", self.on_button_release)
+ self.connect("motion_notify_event", self.draw_cursor)
+ self.highlight_start, self.highlight_end = None, None
+ self.drag_start = None
+ self.move_type = ""
+ self.on_time_changed = None #override this with your func to get notified when user changes date
+ self.on_more_data = None #supplement with more data func that accepts single date
+ self.in_progress = False
+
+ self.range_start = None
+ self.in_motion = False
+ self.days = []
+
+
+ def draw(self, day_facts, highlight = None):
+ """Draw chart with given data"""
+ self.facts = day_facts
+ if self.facts:
+ self.days.append(self.facts[0]["start_time"].date())
+
+ start_time = highlight[0] - dt.timedelta(minutes = highlight[0].minute) - dt.timedelta(hours = 10)
+
+ if self.range_start:
+ self.range_start.target(start_time)
+ self.scroll_to_range_start()
+ else:
+ self.range_start = graphics.Integrator(start_time, damping = 0.35, attraction = 0.5)
+
+ self.highlight = highlight
+
+ self.show()
+
+ self.redraw_canvas()
+
+
+ def on_button_release(self, area, event):
+ if not self.drag_start:
+ return
+
+ self.drag_start, self.move_type = None, None
+
+ if event.state & gtk.gdk.BUTTON1_MASK:
+ self.__call_parent_time_changed()
+
+ def set_in_progress(self, in_progress):
+ self.in_progress = in_progress
+
+ def __call_parent_time_changed(self):
+ #now calculate back from pixels into minutes
+ start_time = self.highlight[0]
+ end_time = self.highlight[1]
+
+ if self.on_time_changed:
+ self.on_time_changed(start_time, end_time)
+
+ def get_time(self, pixels):
+ minutes = self.get_value_at_pos(x = pixels)
+ return self.range_start.value + dt.timedelta(minutes = minutes)
+
+ def scroll_to_range_start(self):
+ if not self.in_motion:
+ self.in_motion = True
+ gobject.timeout_add(1000 / 30, self.animate_scale)
+
+
+ def animate_scale(self):
+ moving = self.range_start.update() > 5
+
+
+ # check if maybe we are approaching day boundaries and should ask for
+ # more data!
+ if self.on_more_data:
+ now = self.range_start.value
+ date_plus = (now + dt.timedelta(hours = 12 + 2*4 + 1)).date()
+ date_minus = (now - dt.timedelta(hours=1)).date()
+
+ if date_minus != now.date() and date_minus not in self.days:
+ self.facts += self.on_more_data(date_minus)
+ self.days.append(date_minus)
+ elif date_plus != now.date() and date_plus not in self.days:
+ self.facts += self.on_more_data(date_plus)
+ self.days.append(date_plus)
+
+
+ self.redraw_canvas()
+ if moving:
+ return True
+ else:
+ self.in_motion = False
+ return False
+
+
+
+ def draw_cursor(self, area, event):
+ if event.is_hint:
+ x, y, state = event.window.get_pointer()
+ else:
+ x = event.x
+ y = event.y
+ state = event.state
+
+ mouse_down = state & gtk.gdk.BUTTON1_MASK
+
+ #print x, self.highlight_start, self.highlight_end
+ if self.highlight_start != None:
+ start_drag = 10 > (self.highlight_start - x) > -1
+
+ end_drag = 10 > (x - self.highlight_end) > -1
+
+ if start_drag and end_drag:
+ start_drag = abs(x - self.highlight_start) < abs(x - self.highlight_end)
+
+ in_between = self.highlight_start <= x <= self.highlight_end
+ scale = True
+
+ if self.in_progress:
+ end_drag = False
+ in_between = False
+
+ if mouse_down and not self.drag_start:
+ self.drag_start = x
+ if start_drag:
+ self.move_type = "start"
+ elif end_drag:
+ self.move_type = "end"
+ elif in_between:
+ self.move_type = "move"
+ self.drag_start = x - self.highlight_start
+ elif scale:
+ self.move_type = "scale_drag"
+ self.drag_start_time = self.range_start.value
+
+
+ if mouse_down and self.drag_start:
+ start, end = 0, 0
+ if self.move_type and self.move_type != "scale_drag":
+ if self.move_type == "start":
+ if 0 <= x <= self.width:
+ start = x
+ end = self.highlight_end
+ elif self.move_type == "end":
+ if 0 <= x <= self.width:
+ start = self.highlight_start
+ end = x
+ elif self.move_type == "move":
+ width = self.highlight_end - self.highlight_start
+ start = x - self.drag_start
+ start = max(0, min(start, self.width))
+
+ end = start + width
+ if end > self.width:
+ end = self.width
+ start = end - width
+
+ if end - start > 1:
+ self.highlight = (self.get_time(start), self.get_time(end))
+ self.redraw_canvas()
+
+ self.__call_parent_time_changed()
+ else:
+ self.range_start.target(self.drag_start_time +
+ dt.timedelta(minutes = self.get_value_at_pos(x = self.drag_start) - self.get_value_at_pos(x = x)))
+ self.scroll_to_range_start()
+
+
+
+ if start_drag:
+ area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.LEFT_SIDE))
+ elif end_drag:
+ area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.RIGHT_SIDE))
+ elif in_between:
+ area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.FLEUR))
+ else:
+ area.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.ARROW))
+
+
+ def _minutes_from_start(self, date):
+ delta = (date - self.range_start.value)
+ return delta.days * 24 * 60 + delta.seconds / 60
+
+ def _render(self):
+ context = self.context
+ #TODO - use system colors and fonts
+
+ context.set_line_width(1)
+
+ #we will buffer 4 hours to both sides so partial labels also appear
+ range_end = self.range_start.value + dt.timedelta(hours = 12 + 2 * 4)
+ self.graph_x = -self.width / 3 #so x moves one third out of screen
+ self.set_value_range(x_min = 0, x_max = 12 * 60)
+
+ minutes = self._minutes_from_start(range_end)
+
+
+
+ graph_y = 4
+ graph_height = self.height - 10
+ graph_y2 = graph_y + graph_height
+
+
+ # graph area
+ self.fill_area(0, graph_y - 1, self.width, graph_height, (1,1,1))
+
+ #bars
+ for fact in self.facts:
+ start_minutes = self._minutes_from_start(fact["start_time"])
+
+ if fact["end_time"]:
+ end_minutes = self._minutes_from_start(fact["end_time"])
+ else:
+ if fact["start_time"].date() > dt.date.today() - dt.timedelta(days=1):
+ end_minutes = self._minutes_from_start(dt.datetime.now())
+ else:
+ end_minutes = start_minutes
+
+ if self.get_pixel(end_minutes) > 0 and \
+ self.get_pixel(start_minutes) < self.width:
+ context.set_source_rgba(0.86, 0.86, 0.86, 0.5)
+
+ context.rectangle(round(self.get_pixel(start_minutes)),
+ graph_y,
+ round(self.get_pixel(end_minutes) - self.get_pixel(start_minutes)),
+ graph_height - 1)
+ context.fill()
+ context.stroke()
+
+ context.set_source_rgba(0.86, 0.86, 0.86, 1)
+ self.move_to(start_minutes, graph_y)
+ self.line_to(start_minutes, graph_y2)
+ self.move_to(end_minutes, graph_y)
+ self.line_to(end_minutes, graph_y2)
+ context.stroke()
+
+
+
+ #time scale
+ context.set_source_rgb(0, 0, 0)
+ self.layout.set_width(-1)
+ for i in range(minutes):
+ label_time = (self.range_start.value + dt.timedelta(minutes=i))
+
+ if label_time.minute == 0:
+ context.set_source_rgb(0.8, 0.8, 0.8)
+ self.move_to(i, graph_y2 - 15)
+ self.line_to(i, graph_y2)
+ context.stroke()
+ elif label_time.minute % 15 == 0:
+ context.set_source_rgb(0.8, 0.8, 0.8)
+ self.move_to(i, graph_y2 - 5)
+ self.line_to(i, graph_y2)
+ context.stroke()
+
+
+
+ if label_time.minute == 0 and label_time.hour % 2 == 0:
+ if label_time.hour == 0:
+ context.set_source_rgb(0.8, 0.8, 0.8)
+ self.move_to(i, graph_y)
+ self.line_to(i, graph_y2)
+ label_minutes = label_time.strftime("%b %d")
+ else:
+ label_minutes = label_time.strftime("%H<small><sup>%M</sup></small>")
+
+ context.set_source_rgb(0.4, 0.4, 0.4)
+ self.layout.set_markup(label_minutes)
+ label_w, label_h = self.layout.get_pixel_size()
+
+ context.move_to(self.get_pixel(i) + 2, graph_y2 - label_h - 8)
+
+ context.show_layout(self.layout)
+ context.stroke()
+
+ #highlight rectangle
+ if self.highlight:
+ self.highlight_start = self.get_pixel(self._minutes_from_start(self.highlight[0]))
+ self.highlight_end = self.get_pixel(self._minutes_from_start(self.highlight[1]))
+
+ #TODO - make a proper range check here
+ if self.highlight_end > 0 and self.highlight_start < self.width:
+ rgb = colorsys.hls_to_rgb(.6, .7, .5)
+
+
+ self.fill_area(self.highlight_start, graph_y,
+ self.highlight_end - self.highlight_start, graph_height,
+ (rgb[0], rgb[1], rgb[2], 0.5))
+ context.stroke()
+
+ context.set_source_rgb(*rgb)
+ self.context.move_to(self.highlight_start, graph_y)
+ self.context.line_to(self.highlight_start, graph_y + graph_height)
+ self.context.move_to(self.highlight_end, graph_y)
+ self.context.line_to(self.highlight_end, graph_y + graph_height)
+ context.stroke()
+
+ #and now put a frame around the whole thing
+ context.set_source_rgb(0.7, 0.7, 0.7)
+ context.rectangle(0, graph_y-1, self.width - 1, graph_height)
+ context.stroke()
+
+ if self.move_type == "move" and (self.highlight_start == 0 or self.highlight_end == self.width):
+ if self.highlight_start == 0:
+ self.range_start.target(self.range_start.value - dt.timedelta(minutes=30))
+ if self.highlight_end == self.width:
+ self.range_start.target(self.range_start.value + dt.timedelta(minutes=30))
+ self.scroll_to_range_start()
+
+
diff --git a/hamster/widgets/reportchooserdialog.py b/hamster/widgets/reportchooserdialog.py
new file mode 100644
index 0000000..c27e29c
--- /dev/null
+++ b/hamster/widgets/reportchooserdialog.py
@@ -0,0 +1,167 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with Project Hamster. If not, see <http://www.gnu.org/licenses/>.
+
+
+import pygtk
+pygtk.require('2.0')
+
+import os
+import gtk, gobject
+
+class ReportChooserDialog(gtk.Dialog):
+ __gsignals__ = {
+ # format, path, start_date, end_date
+ 'report-chosen': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+ (gobject.TYPE_STRING, gobject.TYPE_STRING,
+ gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
+ gobject.TYPE_PYOBJECT)),
+ 'report-chooser-closed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+ }
+ def __init__(self):
+ gtk.Dialog.__init__(self)
+ ui = stuff.load_ui_file("stats.ui")
+ self.dialog = ui.get_object('save_report_dialog')
+
+ self.dialog.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
+ self.dialog.set_current_folder(os.path.expanduser("~"))
+
+ self.filters = {}
+
+ filter = gtk.FileFilter()
+ filter.set_name(_("HTML Report"))
+ filter.add_mime_type("text/html")
+ filter.add_pattern("*.html")
+ filter.add_pattern("*.htm")
+ self.filters[filter] = "html"
+ self.dialog.add_filter(filter)
+
+ filter = gtk.FileFilter()
+ filter.set_name(_("Tab-Separated Values (TSV)"))
+ filter.add_mime_type("text/plain")
+ filter.add_pattern("*.tsv")
+ filter.add_pattern("*.txt")
+ self.filters[filter] = "tsv"
+ self.dialog.add_filter(filter)
+
+ filter = gtk.FileFilter()
+ filter.set_name(_("XML"))
+ filter.add_mime_type("text/xml")
+ filter.add_pattern("*.xml")
+ self.filters[filter] = "xml"
+ self.dialog.add_filter(filter)
+
+ filter = gtk.FileFilter()
+ filter.set_name(_("iCal"))
+ filter.add_mime_type("text/calendar")
+ filter.add_pattern("*.ics")
+ self.filters[filter] = "ical"
+ self.dialog.add_filter(filter)
+
+ filter = gtk.FileFilter()
+ filter.set_name("All files")
+ filter.add_pattern("*")
+ self.dialog.add_filter(filter)
+
+ self.start_date = widgets.DateInput()
+ ui.get_object('from_date_box').add(self.start_date)
+ self.end_date = widgets.DateInput()
+ ui.get_object('to_date_box').add(self.end_date)
+
+ self.category_box = ui.get_object('category_box')
+
+ ui.get_object('save_button').connect("clicked", self.on_save_button_clicked)
+ ui.get_object('cancel_button').connect("clicked", self.on_cancel_button_clicked)
+
+
+ def show(self, start_date, end_date):
+ #set suggested name to something readable, replace backslashes with dots
+ #so the name is valid in linux
+ filename = "Time track %s - %s." % (start_date.strftime("%x").replace("/", "."),
+ end_date.strftime("%x").replace("/", "."))
+ self.dialog.set_current_name(filename)
+
+ self.start_date.set_date(start_date)
+ self.end_date.set_date(end_date)
+
+ #add unsorted category
+ button_all = gtk.CheckButton(C_("categories", "All").encode("utf-8"))
+ button_all.value = None
+ button_all.set_active(True)
+
+ def on_category_all_clicked(checkbox):
+ active = checkbox.get_active()
+ for checkbox in self.category_box.get_children():
+ checkbox.set_active(active)
+
+ button_all.connect("clicked", on_category_all_clicked)
+ self.category_box.attach(button_all, 0, 1, 0, 1)
+
+ categories = runtime.storage.get_category_list()
+ col, row = 0, 0
+ for category in categories:
+ col +=1
+ if col % 4 == 0:
+ col = 0
+ row +=1
+
+ button = gtk.CheckButton(category['name'].encode("utf-8"))
+ button.value = category['id']
+ button.set_active(True)
+ self.category_box.attach(button, col, col+1, row, row+1)
+
+
+
+ response = self.dialog.show_all()
+
+ def present(self):
+ self.dialog.present()
+
+ def on_save_button_clicked(self, widget):
+ path, format = None, None
+
+ format = "html"
+ if self.dialog.get_filter() in self.filters:
+ format = self.filters[self.dialog.get_filter()]
+ path = self.dialog.get_filename()
+
+ # append correct extension if it is missing
+ # TODO - proper way would be to change extension on filter change
+ # only pointer in web is http://www.mail-archive.com/pygtk daa com au/msg08740.html
+ if path.endswith(".%s" % format) == False:
+ path = "%s.%s" % (path.rstrip("."), format)
+
+ categories = []
+ for button in self.category_box.get_children():
+ if button.get_active():
+ categories.append(button.value)
+
+ if None in categories:
+ categories = None # nothing is everything
+
+ # format, path, start_date, end_date
+ self.emit("report-chosen", format, path,
+ self.start_date.get_date().date(),
+ self.end_date.get_date().date(),
+ categories)
+ self.dialog.destroy()
+
+
+ def on_cancel_button_clicked(self, widget):
+ self.emit("report-chooser-closed")
+ self.dialog.destroy()
diff --git a/hamster/widgets/timeline.py b/hamster/widgets/timeline.py
new file mode 100644
index 0000000..c3fc508
--- /dev/null
+++ b/hamster/widgets/timeline.py
@@ -0,0 +1,174 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2009 Toms Bauģis <toms.baugis at gmail.com>
+
+# This file is part of Project Hamster.
+
+# Project Hamster is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# Project Hamster is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with Project Hamster. If not, see <http://www.gnu.org/licenses/>.
+
+from hamster import graphics
+import datetime as dt
+
+class TimeLine(graphics.Area):
+ MODE_YEAR = 0
+ MODE_MONTH = 1
+ MODE_WEEK = 1
+ MODE_DAY = 3
+ def __init__(self):
+ graphics.Area.__init__(self)
+ self.start_date, self.end_date = None, None
+ self.draw_mode = None
+ self.max_hours = None
+
+
+ def draw(self, facts):
+ import itertools
+ self.facts = {}
+ for date, date_facts in itertools.groupby(facts, lambda x: x["start_time"].date()):
+ date_facts = list(date_facts)
+ self.facts[date] = date_facts
+ self.max_hours = max(self.max_hours,
+ sum([fact["delta"].seconds / 60 / float(60) +
+ fact["delta"].days * 24 for fact in date_facts]))
+
+ start_date = facts[0]["start_time"].date()
+ end_date = facts[-1]["start_time"].date()
+
+ self.draw_mode = self.MODE_YEAR
+ self.start_date = start_date.replace(month=1, day=1)
+ self.end_date = end_date.replace(month=12, day=31)
+
+
+ """
+ #TODO - for now we have only the year mode
+ if start_date.year != end_date.year or start_date.month != end_date.month:
+ self.draw_mode = self.MODE_YEAR
+ self.start_date = start_date.replace(month=1, day=1)
+ self.end_date = end_date.replace(month=12, day=31)
+ elif start_date.strftime("%W") != end_date.strftime("%W"):
+ self.draw_mode = self.MODE_MONTH
+ self.start_date = start_date.replace(day=1)
+ self.end_date = end_date.replace(date =
+ calendar.monthrange(self.end_date.year,
+ self.end_date.month)[1])
+ elif start_date != end_date:
+ self.draw_mode = self.MODE_WEEK
+ else:
+ self.draw_mode = self.MODE_DAY
+ """
+
+ self.redraw_canvas()
+
+
+ def _render(self):
+ import calendar
+
+ if self.draw_mode != self.MODE_YEAR:
+ return
+
+ self.fill_area(0, 0, self.width, self.height, (0.975,0.975,0.975))
+ self.set_color((100,100,100))
+
+ self.set_value_range(x_min = 1, x_max = (self.end_date - self.start_date).days)
+ month_label_fits = True
+ for month in range(1, 13):
+ self.layout.set_text(calendar.month_abbr[month])
+ label_w, label_h = self.layout.get_pixel_size()
+ if label_w * 2 > self.x_factor * 30:
+ month_label_fits = False
+ break
+
+
+ ticker_date = self.start_date
+
+ year_pos = 0
+
+ for year in range(self.start_date.year, self.end_date.year + 1):
+ #due to how things lay over, we are putting labels on backwards, so that they don't overlap
+
+ self.context.set_line_width(1)
+ for month in range(1, 13):
+ for day in range(1, calendar.monthrange(year, month)[1] + 1):
+ ticker_pos = year_pos + ticker_date.timetuple().tm_yday
+
+ #if ticker_date.weekday() in [0, 6]:
+ # self.fill_area(ticker_pos * self.x_factor + 1, 20, self.x_factor, self.height - 20, (240, 240, 240))
+ # self.context.stroke()
+
+
+ if self.x_factor > 5:
+ self.move_to(ticker_pos, self.height - 20)
+ self.line_to(ticker_pos, self.height)
+
+ self.layout.set_text(ticker_date.strftime("%d"))
+ label_w, label_h = self.layout.get_pixel_size()
+
+ if label_w < self.x_factor / 1.2: #if label fits
+ self.context.move_to(self.get_pixel(ticker_pos) + 2,
+ self.height - 20)
+ self.context.show_layout(self.layout)
+
+ self.context.stroke()
+
+ #now facts
+ facts_today = self.facts.get(ticker_date, [])
+ if facts_today:
+ total_length = dt.timedelta()
+ for fact in facts_today:
+ total_length += fact["delta"]
+ total_length = total_length.seconds / 60 / 60.0 + total_length.days * 24
+ total_length = total_length / float(self.max_hours) * self.height - 16
+
+ self.fill_area(round(ticker_pos * self.x_factor),
+ round(self.height - total_length),
+ round(self.x_factor),
+ round(total_length),
+ (190,190,190))
+
+
+
+
+ ticker_date += dt.timedelta(1)
+
+
+
+ if month_label_fits:
+ #roll back a little
+ month_pos = ticker_pos - calendar.monthrange(year, month)[1] + 1
+
+ self.move_to(month_pos, 0)
+ #self.line_to(month_pos, 20)
+
+ self.layout.set_text(dt.date(year, month, 1).strftime("%b"))
+
+ self.move_to(month_pos, 0)
+ self.context.show_layout(self.layout)
+
+
+
+
+
+ self.layout.set_text("%d" % year)
+ label_w, label_h = self.layout.get_pixel_size()
+
+ self.move_to(year_pos + 2 / self.x_factor, month_label_fits * label_h * 1.2)
+
+ self.context.show_layout(self.layout)
+
+ self.context.stroke()
+
+ year_pos = ticker_pos #save current state for next year
+
+
+
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]