def set_fact(self, fact): """Set current fact.""" self.fact = fact time_label = fact.start_time.strftime("%H:%M -") if fact.end_time: time_label += fact.end_time.strftime(" %H:%M") self.time_label.set_text(time_label) self.activity_label.set_text(stuff.escape_pango(fact.activity)) category_text = " - {}".format(stuff.escape_pango( fact.category)) if fact.category else "" self.category_label.set_text(category_text) text = stuff.escape_pango(fact.description) description_text = "<small><i>{}</i></small>".format( text) if fact.description else "" self.description_label.set_text(description_text) if fact.tags: # for now, tags are on a single line. # The first one is enough to determine the height. self.tag_label.set_text(stuff.escape_pango(fact.tags[0]))
def _draw(self, context, opacity, matrix): g = graphics.Graphics(context) g.save_context() g.translate(self.x, self.y) # arbitrary 3/4 total width for label, 1/4 for histogram hist_width = self.alloc_w // 4; margin = 10 # pixels label_width = self.alloc_w - hist_width - margin self.layout.set_width(label_width * pango.SCALE) label_h = self.label_height bar_start_x = label_width + margin for i, (label, value) in enumerate(self.values): g.set_color("#333") duration_str = stuff.format_duration(value, human=False) markup_label = stuff.escape_pango(str(label)) markup_duration = stuff.escape_pango(duration_str) self.layout.set_markup("{}, <i>{}</i>".format(markup_label, markup_duration)) y = int(i * label_h * 1.5) g.move_to(0, y) pangocairo.show_layout(context, self.layout) if self._max > dt.timedelta(0): w = ceil(hist_width * value.total_seconds() / self._max.total_seconds()) else: w = 1 g.rectangle(bar_start_x, y, int(w), int(label_h)) g.fill("#999") g.restore_context()
def set_text(self, activity, duration): label = stuff.escape_pango(activity) if len(activity) > 25: #ellipsize at some random length label = "%s%s" % (stuff.escape_pango(activity[:25]), "…") self.activity = label self.duration = duration self.reformat_label()
def show(self, g, colors, fact, current=False): g.save_context() color, bg = colors["normal"], colors["normal_bg"] if current: color, bg = colors["selected"], colors["selected_bg"] g.fill_area(0, 0, self.width, self.height(fact), bg) g.translate(5, 2) time_label = fact.start_time.strftime("%H:%M -") if fact.end_time: time_label += fact.end_time.strftime(" %H:%M") g.set_color(color) self.time_label.show(g, time_label) self.activity_label.show(g, stuff.escape_pango(fact.activity)) if fact.category: g.save_context() g.set_color(color if current else "#999") x = self.activity_label.x + self.activity_label.layout.get_pixel_size( )[0] self.category_label.show(g, " - %s" % stuff.escape_pango(fact.category), x=x, y=2) g.restore_context() if fact.description or fact.tags: g.save_context() g.translate(self.activity_label.x, self.activity_label.height + 3) if fact.tags: self._show_tags(g, fact.tags, color, bg) g.translate(0, self.tag_label.height + 5) if fact.description: self.description_label.show( g, "<small>%s</small>" % stuff.escape_pango(fact.description)) g.restore_context() self.duration_label.show(g, stuff.format_duration(fact.delta), x=self.width - 105) g.restore_context()
def _draw(self, context, opacity, matrix): g = graphics.Graphics(context) g.save_context() g.translate(self.x, self.y) for i, (label, value) in enumerate(self.values): g.set_color("#333") duration_str = stuff.format_duration(value, human=False) markup = stuff.escape_pango('{}, {}'.format(label, duration_str)) self.layout.set_markup(markup) label_w, label_h = self.layout.get_pixel_size() bar_start_x = 150 # pixels margin = 10 # pixels y = int(i * label_h * 1.5) g.move_to(bar_start_x - margin - label_w, y) pangocairo.show_layout(context, self.layout) if self._max > dt.timedelta(0): w = ceil((self.alloc_w - bar_start_x) * value.total_seconds() / self._max.total_seconds()) else: w = 1 g.rectangle(bar_start_x, y, int(w), int(label_h)) g.fill("#999") g.restore_context()
def plot(self, keys, data): self.data = data bars = dict([(bar.key, bar.normalized) for bar in self.bars]) max_val = float(max(data or [0])) new_bars, new_labels = [], [] for key, value in zip(keys, data): if max_val: normalized = value / max_val else: normalized = 0 bar = Bar(key, locale.format(self.value_format, value), normalized, self.label_color) bar.interactive = self.graph_interactive if key in bars: bar.normalized = bars[key] self.tweener.add_tween(bar, normalized=normalized) new_bars.append(bar) label = graphics.Label(stuff.escape_pango(key), size = 8, alignment = pango.Alignment.RIGHT) new_labels.append(label) self.plot_area.remove_child(*self.bars) self.remove_child(*self.labels) self.bars, self.labels = new_bars, new_labels self.add_child(*self.labels) self.plot_area.add_child(*self.bars) self.show() self.redraw()
def _draw(self, context, opacity, matrix): g = graphics.Graphics(context) g.save_context() g.translate(self.x, self.y) hours = [(value.days * 24.0 + value.seconds / 3600.0) for _, value in self.values] total = self._total.days * 24.0 + self._total.seconds / 3600.0 for i, (label, value) in enumerate(self.values): percent = 100.0 * hours[i] / total label += " (%3.2fh; %3.1f%%)" % (hours[i], percent) g.set_color("#333") self.layout.set_markup(stuff.escape_pango(label)) label_w, label_h = self.layout.get_pixel_size() y = int(i * label_h * 1.5) g.move_to(180 - label_w, y) pangocairo.show_layout(context, self.layout) w = (self.alloc_w - 200) * value.total_seconds() / self._max.total_seconds() w = max(1, int(round(w))) g.rectangle(190, y, int(w), int(label_h)) g.fill("#999") g.restore_context()
def set_facts(self, facts): totals = defaultdict(lambda: defaultdict(dt.timedelta)) total_sums = defaultdict(lambda: dt.timedelta()) for fact in facts: for key in ('category', 'activity'): totals[key][getattr(fact, key).strip()] += fact.delta total_sums[key] += fact.delta for tag in fact.tags: totals["tag"][tag.strip()] += fact.delta total_sums["tag"] += fact.delta for key, group in totals.iteritems(): totals[key] = sorted(group.iteritems(), key=lambda x: x[1], reverse=True) self.totals = totals self.activities_chart.set_values(totals['activity']) self.activities_chart.set_total(total_sums['activity']) self.categories_chart.set_values(totals['category']) self.categories_chart.set_total(total_sums['category']) self.tag_chart.set_values(totals['tag']) self.tag_chart.set_total(total_sums['tag']) self.stacked_bar.set_items([(cat, delta.total_seconds() / 60.0) for cat, delta in totals['category']]) self.category_totals.markup = ", ".join( "<b>%s:</b> %s" % (stuff.escape_pango(cat), stuff.format_duration(hours)) for cat, hours in totals['category'])
def __init__(self, width, fact, color, **kwargs): graphics.Sprite.__init__(self, **kwargs) self.width = width self.height = 27 self.natural_height = 27 self.fact = fact self.color = color self.interactive = True self.mouse_cursor = gdk.CursorType.XTERM self.fact_labels = graphics.Sprite() self.start_label = graphics.Label("", color="#333", size=11, x=10, y=5, interactive=True, mouse_cursor=gdk.CursorType.XTERM) self.start_label.text = "%s - " % fact.start_time.strftime("%H:%M") self.fact_labels.add_child(self.start_label) self.end_label = graphics.Label("", color="#333", size=11, x=65, y=5, interactive=True, mouse_cursor=gdk.CursorType.XTERM) if fact.end_time: self.end_label.text = fact.end_time.strftime("%H:%M") self.fact_labels.add_child(self.end_label) self.activity_label = graphics.Label(fact.activity, color="#333", size=11, x=120, y=5, interactive=True, mouse_cursor=gdk.CursorType.XTERM) self.fact_labels.add_child(self.activity_label) self.category_label = graphics.Label("", color="#333", size=9, y=7, interactive=True, mouse_cursor=gdk.CursorType.XTERM) self.category_label.text = stuff.escape_pango(" - %s" % fact.category) self.category_label.x = self.activity_label.x + self.activity_label.width self.fact_labels.add_child(self.category_label) self.duration_label = graphics.Label(stuff.format_duration(fact.delta), size=11, color="#333", interactive=True, mouse_cursor=gdk.CursorType.XTERM) self.duration_label.x = self.width - self.duration_label.width - 5 self.duration_label.y = 5 self.fact_labels.add_child(self.duration_label) self.add_child(self.fact_labels) self.edit_links = graphics.Sprite(x=10, y = 110, opacity=0) self.delete_link = graphics.Label("Delete", size=11, color="#555", interactive=True) self.save_link = graphics.Label("Save", size=11, x=390, color="#555", interactive=True) self.cancel_link = graphics.Label("Cancel", size=11, x=440, color="#555", interactive=True) self.edit_links.add_child(self.delete_link, self.save_link, self.cancel_link) self.add_child(self.edit_links) for sprite in self.fact_labels.sprites: sprite.connect("on-click", self.on_sprite_click) self.connect("on-render", self.on_render) self.connect("on-click", self.on_click)
def add_action(self, name, text): """Add an action to the suggestions. name (str): unique label, use to retrieve the action index. text (str): text used to display the action. """ markup = "<i>{}</i>".format(stuff.escape_pango(text)) idx = len(self._action_list) self.completion.insert_action_markup(idx, markup) self._action_list.append(name)
def _clamp_text(self, text, length=25, with_ellipsis=True, is_indicator=False): text = stuff.escape_pango(text) if len(text) > length: #ellipsize at some random length if with_ellipsis: if is_indicator: text = "%s%s" % (text[:length], "...") else: text = "%s%s" % (text[:length], "…") else: text = "%s" % (text[:length]) return text
def show(self, g, colors, fact, current=False): g.save_context() color, bg = colors["normal"], colors["normal_bg"] if current: color, bg = colors["selected"], colors["selected_bg"] g.fill_area(0, 0, self.width, self.height(fact), bg) g.translate(5, 2) time_label = fact.start_time.strftime("%H:%M -") if fact.end_time: time_label += fact.end_time.strftime(" %H:%M") g.set_color(color) self.time_label.show(g, time_label) self.activity_label.show(g, stuff.escape_pango(fact.activity)) if fact.category: g.save_context() g.set_color(color if current else "#999") x = self.activity_label.x + self.activity_label.layout.get_pixel_size()[0] self.category_label.show(g, " - %s" % stuff.escape_pango(fact.category), x=x, y=2) g.restore_context() if fact.description or fact.tags: g.save_context() g.translate(self.activity_label.x, self.activity_label.height + 3) if fact.tags: self._show_tags(g, fact.tags, color, bg) g.translate(0, self.tag_label.height + 5) if fact.description: self.description_label.show(g, "<small>%s</small>" % stuff.escape_pango(fact.description)) g.restore_context() self.duration_label.show(g, stuff.format_duration(fact.delta), x=self.width - 105) g.restore_context()
def __init__(self, text, interactive = True, color = "#F1EAAA"): graphics.Sprite.__init__(self, interactive = interactive) self.width, self.height = 0,0 font = gtk.Style().font_desc font_size = int(font.get_size() * 0.8 / pango.SCALE) # 80% of default self.label = graphics.Label(text, size = font_size, color = (30, 30, 30), y = 1) self.color = color self.add_child(self.label) self.corner = int((self.label.height + 3) / 3) + 0.5 self.label.x = self.corner + 6 self.text = stuff.escape_pango(text) self.connect("on-render", self.on_render)
def _show_tags(self, g, color, bg): label = self.tag_label label.color = bg g.save_context() g.translate(self.tag_row_margin_H, self.tag_row_margin_V) for tag in self.fact.tags: label.set_text(stuff.escape_pango(tag)) w, h = label.layout.get_pixel_size() rw = w + self.tag_inner_margin_H * 2 rh = h + self.tag_inner_margin_V * 2 g.rectangle(0, 0, rw, rh, 2) g.fill(color, 0.5) label.show(g, x=self.tag_inner_margin_H, y=self.tag_inner_margin_V) g.translate(rw + self.inter_tag_margin, 0) g.restore_context()
def _draw(self, context, opacity, matrix): g = graphics.Graphics(context) g.save_context() g.translate(self.x, self.y) for i, (label, value) in enumerate(self.values): g.set_color("#333") self.layout.set_markup(stuff.escape_pango(label)) label_w, label_h = self.layout.get_pixel_size() y = int(i * label_h * 1.5) g.move_to(100 - label_w, y) pangocairo.show_layout(context, self.layout) w = (self.alloc_w - 110) * value.total_seconds() / self._max.total_seconds() w = max(1, int(round(w))) g.rectangle(110, y, int(w), int(label_h)) g.fill("#999") g.restore_context()
def set_facts(self, facts): totals = defaultdict(lambda: defaultdict(dt.timedelta)) for fact in facts: for key in ('category', 'activity'): totals[key][getattr(fact, key)] += fact.delta for tag in fact.tags: totals["tag"][tag] += fact.delta for key, group in totals.iteritems(): totals[key] = sorted(group.iteritems(), key=lambda x: x[1], reverse=True) self.totals = totals self.activities_chart.set_values(totals['activity']) self.categories_chart.set_values(totals['category']) self.tag_chart.set_values(totals['tag']) self.stacked_bar.set_items([(cat, delta.total_seconds() / 60.0) for cat, delta in totals['category']]) self.category_totals.markup = ", ".join("<b>%s:</b> %s" % (stuff.escape_pango(cat), stuff.format_duration(hours)) for cat, hours in totals['category'])
def __init__(self, text, interactive=True, color="#F1EAAA"): graphics.Sprite.__init__(self, interactive=interactive) self.width, self.height = 0, 0 font = gtk.Style().font_desc font_size = int(font.get_size() * 0.8 / pango.SCALE) # 80% of default self.label = graphics.Label(text, size=font_size, color=(30, 30, 30), y=1) self.color = color self.add_child(self.label) self.corner = int((self.label.height + 3) / 3) + 0.5 self.label.x = self.corner + 6 self.text = stuff.escape_pango(text) self.connect("on-render", self.on_render)
def __init__(self, width, fact, color, **kwargs): graphics.Sprite.__init__(self, **kwargs) self.width = width self.height = 27 self.natural_height = 27 self.fact = fact self.color = color self.interactive = True self.mouse_cursor = gdk.CursorType.XTERM self.fact_labels = graphics.Sprite() self.start_label = graphics.Label("", color="#333", size=11, x=10, y=5, interactive=True, mouse_cursor=gdk.CursorType.XTERM) self.start_label.text = "%s - " % fact.start_time.strftime("%H:%M") self.fact_labels.add_child(self.start_label) self.end_label = graphics.Label("", color="#333", size=11, x=65, y=5, interactive=True, mouse_cursor=gdk.CursorType.XTERM) if fact.end_time: self.end_label.text = fact.end_time.strftime("%H:%M") self.fact_labels.add_child(self.end_label) self.activity_label = graphics.Label(fact.activity, color="#333", size=11, x=120, y=5, interactive=True, mouse_cursor=gdk.CursorType.XTERM) self.fact_labels.add_child(self.activity_label) self.category_label = graphics.Label("", color="#333", size=9, y=7, interactive=True, mouse_cursor=gdk.CursorType.XTERM) self.category_label.text = stuff.escape_pango(" - %s" % fact.category) self.category_label.x = self.activity_label.x + self.activity_label.width self.fact_labels.add_child(self.category_label) self.duration_label = graphics.Label(stuff.format_duration(fact.delta), size=11, color="#333", interactive=True, mouse_cursor=gdk.CursorType.XTERM) self.duration_label.x = self.width - self.duration_label.width - 5 self.duration_label.y = 5 self.fact_labels.add_child(self.duration_label) self.add_child(self.fact_labels) self.edit_links = graphics.Sprite(x=10, y=110, opacity=0) self.delete_link = graphics.Label("Delete", size=11, color="#555", interactive=True) self.save_link = graphics.Label("Save", size=11, x=390, color="#555", interactive=True) self.cancel_link = graphics.Label("Cancel", size=11, x=440, color="#555", interactive=True) self.edit_links.add_child(self.delete_link, self.save_link, self.cancel_link) self.add_child(self.edit_links) for sprite in self.fact_labels.sprites: sprite.connect("on-click", self.on_sprite_click) self.connect("on-render", self.on_render) self.connect("on-click", self.on_click)
def validate_fields(self): """Check fields information. Update gui status about entry and description validity. Try to merge date, activity and description informations. Return the consolidated fact if successful, or None. """ fact = self.localized_fact() now = hamster_now() self.get_widget("button-next-day").set_sensitive( self.date < now.date()) if self.date == now.date(): default_dt = now else: default_dt = dt.datetime.combine(self.date, now.time()) self.draw_preview(fact.start_time or default_dt, fact.end_time or default_dt) if fact.start_time is None: self.update_status(status="wrong", markup="Missing start time") return None if not fact.activity: self.update_status(status="wrong", markup="Missing activity") return None description_box_content = self.figure_description() if fact.description and description_box_content: escaped_cmd = escape_pango(fact.description) escaped_box = escape_pango(description_box_content) markup = dedent("""\ <b>Duplicate description</b> <i>command line</i>: '{}' <i>description box</i>: '''{}''' """).format(escaped_cmd, escaped_box) self.update_status(status="wrong", markup=markup) return None # Good to go, no description ambiguity if description_box_content: fact.description = description_box_content if (fact.delta < dt.timedelta(0)) and fact.end_time: fact.end_time += dt.timedelta(days=1) markup = dedent("""\ <b>Working late ?</b> Duration would be negative. This happens when the activity crosses the hamster day start time ({:%H:%M} from tracking settings). Changing the end time date to the next day. Pressing the button would save an actvity going from {} to {} (in civil local time) """.format(conf.day_start, fact.start_time, fact.end_time)) self.update_status(status="warning", markup=markup) return fact # nothing unusual self.update_status(status="looks good", markup="") return fact
def update_suggestions(self, text=""): """ * from previous activity | set time | minutes ago | start now * to ongoing | set time * activity * [@category] * #tags, #tags, #tags * we will leave description for later all our magic is space separated, strictly, start-end can be just dash phases: [start_time] | [-end_time] | activity | [@category] | [#tag] """ now = dt.datetime.now() text = text.lstrip() time_re = re.compile("^([0-1]?[0-9]|[2][0-3]):([0-5][0-9])$") time_range_re = re.compile("^([0-1]?[0-9]|[2][0-3]):([0-5][0-9])-([0-1]?[0-9]|[2][0-3]):([0-5][0-9])$") delta_re = re.compile("^-[0-9]{1,3}$") # when the time is filled, we need to make sure that the chunks parse correctly delta_fragment_re = re.compile("^-[0-9]{0,3}$") templates = { "start_time": "", "start_delta": ("start activity -n minutes ago", "-"), } # need to set the start_time template before prev_fact = self.todays_facts[-1] if self.todays_facts else None if prev_fact and prev_fact.end_time: templates["start_time"] = ("from previous activity %s ago" % stuff.format_duration(now - prev_fact.end_time), prev_fact.end_time.strftime("%H:%M ")) variants = [] fact = Fact(text) # figure out what we are looking for # time -> activity[@category] -> tags -> description # presence of each next attribute means that we are not looking for the previous one # we still might be looking for the current one though looking_for = "start_time" fields = ["start_time", "end_time", "activity", "category", "tags", "description", "done"] for field in reversed(fields): if getattr(fact, field, None): looking_for = field if text[-1] == " ": looking_for = fields[fields.index(field)+1] break fragments = [f for f in re.split("[\s|#]", text)] current_fragment = fragments[-1] if fragments else "" if not text.strip(): variants = [templates[name] for name in ("start_time", "start_delta") if templates[name]] elif looking_for == "start_time" and text == "-": if len(current_fragment) > 1: # avoid blank "-" templates["start_delta"] = ("%s minutes ago" % (-int(current_fragment)), current_fragment) variants.append(templates["start_delta"]) res = [] for (description, variant) in variants: res.append(DataRow(variant, description=description)) # regular activity if (looking_for in ("start_time", "end_time") and not looks_like_time(text.split(" ")[-1])) or \ looking_for in ("activity", "category"): search = extract_search(text) matches = [] for match, score in self.suggestions: if search in match: if match.startswith(search): score += 10**8 # boost beginnings matches.append((match, score)) matches = sorted(matches, key=lambda x: x[1], reverse=True)[:7] # need to limit these guys, sorry for match, score in matches: label = (fact.start_time or now).strftime("%H:%M") if fact.end_time: label += fact.end_time.strftime("-%H:%M") markup_label = label + " " + (stuff.escape_pango(match).replace(search, "<b>%s</b>" % search) if search else match) label += " " + match res.append(DataRow(markup_label, match, label)) if not res: # in case of nothing to show, add preview so that the user doesn't # think they are lost label = (fact.start_time or now).strftime("%H:%M") if fact.end_time: label += fact.end_time.strftime("-%H:%M") if fact.activity: label += " " + fact.activity if fact.category: label += "@" + fact.category if fact.tags: label += " #" + " #".join(fact.tags) res.append(DataRow(stuff.escape_pango(label), description="Start tracking")) self.complete_tree.set_rows(res)
def update_suggestions(self, text=""): """ * from previous activity | set time | minutes ago | start now * to ongoing | set time * activity * [@category] * #tags, #tags, #tags * we will leave description for later all our magic is space separated, strictly, start-end can be just dash phases: [start_time] | [-end_time] | activity | [@category] | [#tag] """ res = [] fact = Fact(text) now = dt.datetime.now() # figure out what we are looking for # time -> activity[@category] -> tags -> description # presence of an attribute means that we are not looking for the previous one # we still might be looking for the current one though looking_for = "start_time" fields = ["start_time", "end_time", "activity", "category", "tags", "description", "done"] for field in reversed(fields): if getattr(fact, field, None): looking_for = field if text[-1] == " ": looking_for = fields[fields.index(field)+1] break fragments = [f for f in re.split("[\s|#]", text)] current_fragment = fragments[-1] if fragments else "" search = extract_search(text) matches = [] for match, score in self.suggestions: if search in match: if match.startswith(search): score += 10**8 # boost beginnings matches.append((match, score)) # need to limit these guys, sorry matches = sorted(matches, key=lambda x: x[1], reverse=True)[:7] for match, score in matches: label = (fact.start_time or now).strftime("%H:%M") if fact.end_time: label += fact.end_time.strftime("-%H:%M") markup_label = label + " " + (stuff.escape_pango(match).replace(search, "<b>%s</b>" % search) if search else match) label += " " + match res.append(DataRow(markup_label, match, label)) # list of tuples (description, variant) variants = [] if self.original_fact: # editing an existing fact variant_fact = None if self.original_fact.end_time is None: description = "stop now" variant_fact = self.original_fact.copy() variant_fact.end_time = now elif self.original_fact == self.todays_facts[-1]: # that one is too dangerous, except for the last entry description = "keep up" # Do not use Fact(..., end_time=None): it would be a no-op variant_fact = self.original_fact.copy() variant_fact.end_time = None if variant_fact: variant = variant_fact.serialized(prepend_date=False) variants.append((description, variant)) else: # brand new fact description = "start now" variant = now.strftime("%H:%M ") variants.append((description, variant)) prev_fact = self.todays_facts[-1] if self.todays_facts else None if prev_fact and prev_fact.end_time: since = stuff.format_duration(now - prev_fact.end_time) description = "from previous activity, %s ago" % since variant = prev_fact.end_time.strftime("%H:%M ") variants.append((description, variant)) description = "start activity -n minutes ago (1 or 3 digits allowed)" variant = "-" variants.append((description, variant)) text = text.strip() if text: description = "clear" variant = "" variants.append((description, variant)) for (description, variant) in variants: res.append(DataRow(variant, description=description)) self.complete_tree.set_rows(res)