Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate edit fields #461

Merged
merged 73 commits into from
Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
804f75f
scroll till the end of last fact
ederag Sep 14, 2019
431e422
glade update
ederag Sep 14, 2019
116f578
ignore *~ backup files
ederag Sep 14, 2019
0716eaf
remove deprecated use_action_appearance
ederag Sep 14, 2019
c486590
add separate fields to the window
ederag Sep 14, 2019
b5804f0
control activity/category sizes
ederag Sep 15, 2019
b59d33c
rename activity to cmdline
ederag Sep 14, 2019
af402c6
add calendar widgets
ederag Sep 16, 2019
27afce3
cmdline update activity field
ederag Sep 16, 2019
04296f6
update category field
ederag Sep 16, 2019
c3ac659
update only while typing
ederag Sep 17, 2019
88a06d6
back and forth updates with activity
ederag Sep 18, 2019
10224b0
add tags
ederag Sep 19, 2019
02a46e6
try to fix background color
ederag Sep 21, 2019
b55e9e3
remove background noop
ederag Sep 21, 2019
321469b
update cmdline from category entry
ederag Sep 21, 2019
2987ad6
remove unused self.day_start
ederag Sep 22, 2019
235ab86
update date entries
ederag Sep 22, 2019
62ddfe4
use TimeInput for start and end times
ederag Sep 22, 2019
51f05ab
cmdline update times
ederag Sep 22, 2019
8c6c473
fix start_time suggestions
ederag Sep 23, 2019
e30410b
accept None
ederag Sep 24, 2019
045e866
handle end_time=None
ederag Sep 24, 2019
b176cc3
control date from dayline
ederag Sep 26, 2019
94e4416
comments
ederag Sep 26, 2019
88234f3
forward failing attribute requests to widget
ederag Sep 26, 2019
6644b68
start new fact now in gui
ederag Sep 27, 2019
f52b429
update timeline from start_date
ederag Sep 27, 2019
f642808
update end_date from gui entry
ederag Sep 27, 2019
dbd337b
change TimeInput.time to @property
ederag Sep 27, 2019
0f2fd46
update start_time from gui entry
ederag Sep 27, 2019
f49b271
handle no start-time on cmdline
ederag Sep 27, 2019
949e841
fix time pre-selection
ederag Sep 28, 2019
b6a3c0d
use start_time.date() if available
ederag Sep 28, 2019
80a70ae
update end_time from gui entry
ederag Sep 28, 2019
be73305
update activity and category from gui entries
ederag Sep 28, 2019
147a9bc
rename ActivityEntry to CmdLineEntry
ederag Sep 30, 2019
2c1d57d
add category completion
ederag Sep 30, 2019
e5a1bbf
activity@category completion
ederag Oct 1, 2019
61b7adc
fix description update from cmdline
ederag Oct 2, 2019
e70fd54
remove @category lines
ederag Oct 4, 2019
116b384
use self.fact directly on save
ederag Oct 5, 2019
030bf04
cleanup: move on_save_button_clicked
ederag Oct 5, 2019
20496ec
prevent enter from saving invalid fact
ederag Oct 5, 2019
2fd3513
copy the entered fact before any modification
ederag Oct 5, 2019
ac40301
ensure no end date without time
ederag Oct 5, 2019
324756b
cleanup: remove unused previous_time
ederag Oct 5, 2019
c04a2c3
Do not quit on Esc if any popup is visible
ederag Oct 5, 2019
42fc93e
move keyrelease events to keypress handler
ederag Oct 5, 2019
f05c700
grab focus on icon click
ederag Oct 5, 2019
2c78db4
comments
ederag Oct 5, 2019
1b51b77
use first column for text/filter
ederag Oct 5, 2019
4a5fa54
cleanup: wrap long line
ederag Oct 5, 2019
558a4b2
comment
ederag Oct 5, 2019
888f209
cleanup: remove no-op
ederag Oct 5, 2019
342a8e5
Merge remote-tracking branch 'upstream/master' into separate_edit_fields
ederag Oct 5, 2019
a7e567b
use str in ListStore call
ederag Oct 6, 2019
7e8d0e7
match the parent Entry width
ederag Oct 6, 2019
7eec94c
set no start time interval to 15 min
ederag Oct 6, 2019
3d2109b
accept None in datetime_to_hamsterday
ederag Oct 6, 2019
00df6b7
handle date changes with empty start_time
ederag Oct 6, 2019
aea8352
add clear icon to TimeInput
ederag Oct 6, 2019
d57eb94
move on_description_changed signal connection
ederag Oct 6, 2019
2d18aea
replace gtk.gdk references with gdk
ederag Oct 6, 2019
55a3155
separate down and clear category icons
ederag Oct 20, 2019
577b5bc
add clear action to category completion
ederag Oct 20, 2019
fa5759e
separate icons for TimeInput
ederag Oct 20, 2019
c967463
filter activities on category
ederag Oct 21, 2019
6225d27
remove unused functions
ederag Oct 22, 2019
8ebbc7a
use colorless icon
ederag Oct 22, 2019
63417e8
remove go-down-symbolic icon
ederag Oct 23, 2019
cc55b7e
hide calendars
ederag Oct 29, 2019
43c76d0
set label_fill
ederag Oct 29, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# backups (eg. glade)
*~
*.pyc
autom4te.cache/
m4/
Expand Down
356 changes: 338 additions & 18 deletions data/edit_activity.ui

Large diffs are not rendered by default.

279 changes: 210 additions & 69 deletions src/hamster/edit_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"""
from hamster import widgets
from hamster.lib.configuration import runtime, conf, load_ui_file
from hamster.lib.stuff import hamster_today, hamster_now, escape_pango
from hamster.lib.stuff import (
hamsterday_time_to_datetime, hamster_today, hamster_now, escape_pango)
from hamster.lib import Fact


Expand All @@ -48,64 +49,88 @@ def __init__(self, parent=None, fact_id=None, base_fact=None):
# None if creating a new fact, instead of editing one
self.fact_id = fact_id

self.activity = widgets.ActivityEntry()
self.activity.connect("changed", self.on_activity_changed)
self.get_widget("activity_box").add(self.activity)
self.category_entry = widgets.CategoryEntry(widget=self.get_widget('category'))
self.activity_entry = widgets.ActivityEntry(widget=self.get_widget('activity'),
category_widget=self.category_entry)

self.day_start = conf.day_start
self.cmdline = widgets.CmdLineEntry()
self.get_widget("command line box").add(self.cmdline)
self.cmdline.connect("focus_in_event", self.on_cmdline_focus_in_event)
self.cmdline.connect("focus_out_event", self.on_cmdline_focus_out_event)

self.dayline = widgets.DayLine()
self._gui.get_object("day_preview").add(self.dayline)

self.description_box = self.get_widget('description')
self.description_buffer = self.description_box.get_buffer()
self.description_buffer.connect("changed", self.on_description_changed)

self.end_date = widgets.Calendar(widget=self.get_widget("end date"),
expander=self.get_widget("end date expander"))

self.end_time = widgets.TimeInput()
self.get_widget("end time box").add(self.end_time)

self.start_date = widgets.Calendar(widget=self.get_widget("start date"),
expander=self.get_widget("start date expander"))

self.start_time = widgets.TimeInput()
self.get_widget("start time box").add(self.start_time)

self.tags_entry = widgets.TagsEntry()
self.get_widget("tags box").add(self.tags_entry)

self.save_button = self.get_widget("save_button")

self.activity.grab_focus()
# this will set self.master_is_cmdline
self.cmdline.grab_focus()

if fact_id:
# editing
fact = runtime.storage.get_fact(fact_id)
self.date = fact.date
original_fact = fact
self.fact = runtime.storage.get_fact(fact_id)
self.date = self.fact.date
self.window.set_title(_("Update activity"))
else:
self.date = hamster_today()
self.get_widget("delete_button").set_sensitive(False)
if base_fact:
# start a clone now.
original_fact = base_fact.copy(start_time=hamster_now(),
end_time=None)
self.fact = base_fact.copy(start_time=hamster_now(),
end_time=None)
else:
original_fact = None
self.fact = Fact(start_time=hamster_now())

if original_fact:
stripped_fact = original_fact.copy()
stripped_fact.description = None
label = stripped_fact.serialized(prepend_date=False)
with self.activity.handler_block(self.activity.checker):
self.activity.set_text(label)
time_len = len(label) - len(stripped_fact.serialized_name())
self.activity.select_region(0, time_len - 1)
self.description_buffer.set_text(original_fact.description)
original_fact = self.fact

self.activity.original_fact = original_fact
self.update_fields()
self.update_cmdline(select=True)

self.cmdline.original_fact = original_fact

# This signal should be emitted only after a manual modification,
# not at init time when cmdline might not always be fully parsable.
self.cmdline.connect("changed", self.on_cmdline_changed)
self.description_buffer.connect("changed", self.on_description_changed)
self.start_time.connect("changed", self.on_start_time_changed)
self.start_date.connect("day-selected", self.on_start_date_changed)
self.start_date.expander.connect("activate",
self.on_start_date_expander_activated)
self.end_time.connect("changed", self.on_end_time_changed)
self.end_date.connect("day-selected", self.on_end_date_changed)
self.end_date.expander.connect("activate",
self.on_end_date_expander_activated)
self.activity_entry.connect("changed", self.on_activity_changed)
self.category_entry.connect("changed", self.on_category_changed)
self.tags_entry.connect("changed", self.on_tags_changed)

self._gui.connect_signals(self)
self.validate_fields()
self.window.show_all()

def on_description_changed(self, text):
self.validate_fields()

def on_prev_day_clicked(self, button):
self.date = self.date - dt.timedelta(days=1)
self.validate_fields()
self.increment_date(-1)

def on_next_day_clicked(self, button):
self.date = self.date + dt.timedelta(days=1)
self.validate_fields()
self.increment_date(+1)

def draw_preview(self, start_time, end_time=None):
day_facts = runtime.storage.get_facts(self.date)
Expand All @@ -115,6 +140,15 @@ def get_widget(self, name):
""" skip one variable (huh) """
return self._gui.get_object(name)

def increment_date(self, days):
delta = dt.timedelta(days=days)
self.date += delta
if self.fact.start_time:
self.fact.start_time += delta
if self.fact.end_time:
self.fact.end_time += delta
self.update_fields()

def show(self):
self.window.show()

Expand All @@ -123,24 +157,139 @@ def figure_description(self):
description = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), 0)
return description.strip()

def localized_fact(self):
"""Make sure fact has the correct start_time."""
fact = Fact.parse(self.activity.get_text())
if fact.start_time:
fact.date = self.date
else:
fact.start_time = hamster_now()
return fact

def on_save_button_clicked(self, button):
fact = self.validate_fields()
if self.fact_id:
runtime.storage.update_fact(self.fact_id, fact)
else:
runtime.storage.add_fact(fact)
self.close_window()
def on_activity_changed(self, widget):
if not self.master_is_cmdline:
self.fact.activity = self.activity_entry.get_text()
self.validate_fields()
self.update_cmdline()

def on_category_changed(self, widget):
if not self.master_is_cmdline:
self.fact.category = self.category_entry.get_text()
self.validate_fields()
self.update_cmdline()

def on_cmdline_changed(self, widget):
if self.master_is_cmdline:
fact = Fact.parse(self.cmdline.get_text(), date=self.date)
previous_cmdline_fact = self.cmdline_fact
# copy the entered fact before any modification
self.cmdline_fact = fact.copy()
if fact.start_time is None:
fact.start_time = hamster_now()
if fact.description == previous_cmdline_fact.description:
# no change to description here, keep the main one
fact.description = self.fact.description
self.fact = fact
self.update_fields()

def on_cmdline_focus_in_event(self, widget, event):
self.master_is_cmdline = True

def on_cmdline_focus_out_event(self, widget, event):
self.master_is_cmdline = False

def on_activity_changed(self, combo):
def on_description_changed(self, text):
if not self.master_is_cmdline:
self.fact.description = self.figure_description()
self.validate_fields()
self.update_cmdline()

def on_end_date_changed(self, widget):
if not self.master_is_cmdline:
if self.fact.end_time:
time = self.fact.end_time.time()
self.fact.end_time = dt.datetime.combine(self.end_date.date, time)
self.validate_fields()
self.update_cmdline()
elif self.end_date.date:
# No end time means on-going, hence date would be meaningless.
# And a default end date may be provided when end time is set,
# so there should never be a date without time.
self.end_date.date = None

def on_end_date_expander_activated(self, widget):
# state has not changed yet, toggle also start_date calendar visibility
previous_state = self.end_date.expander.get_expanded()
self.start_date.expander.set_expanded(not previous_state)

def on_end_time_changed(self, widget):
if not self.master_is_cmdline:
# self.end_time.start_time() was given a datetime,
# so self.end_time.time is a datetime too.
end = self.end_time.time
self.fact.end_time = end
self.end_date.date = end.date() if end else None
self.validate_fields()
self.update_cmdline()

def on_start_date_changed(self, widget):
if not self.master_is_cmdline:
if self.fact.start_time:
previous_date = self.fact.start_time.date()
new_date = self.start_date.date
delta = new_date - previous_date
self.fact.start_time += delta
if self.fact.end_time:
# preserve fact duration
self.fact.end_time += delta
self.end_date.date = self.fact.end_time
self.date = self.fact.date or hamster_today()
self.validate_fields()
self.update_cmdline()

def on_start_date_expander_activated(self, widget):
# state has not changed yet, toggle also end_date calendar visibility
previous_state = self.start_date.expander.get_expanded()
self.end_date.expander.set_expanded(not previous_state)

def on_start_time_changed(self, widget):
if not self.master_is_cmdline:
# note: resist the temptation to preserve duration here;
# for instance, end time might be at the beginning of next fact.
new_time = self.start_time.time
if new_time:
if self.fact.start_time:
new_start_time = dt.datetime.combine(self.fact.start_time.date(),
new_time)
else:
# date not specified; result must fall in current hamster_day
new_start_time = hamsterday_time_to_datetime(hamster_today(),
new_time)
else:
new_start_time = None
self.fact.start_time = new_start_time
# let start_date extract date or handle None
self.start_date.date = new_start_time
self.validate_fields()
self.update_cmdline()

def on_tags_changed(self, widget):
if not self.master_is_cmdline:
self.fact.tags = self.tags_entry.get_tags()
self.update_cmdline()

def update_cmdline(self, select=None):
"""Update the cmdline entry content."""
self.cmdline_fact = self.fact.copy(description=None)
label = self.cmdline_fact.serialized(prepend_date=False)
with self.cmdline.handler_block(self.cmdline.checker):
self.cmdline.set_text(label)
if select:
time_str = self.cmdline_fact.serialized_time(prepend_date=False)
self.cmdline.select_region(0, len(time_str))

def update_fields(self):
"""Update gui fields content."""
self.start_time.time = self.fact.start_time
self.end_time.time = self.fact.end_time
self.end_time.set_start_time(self.fact.start_time)
self.start_date.date = self.fact.start_time
self.end_date.date = self.fact.end_time
self.activity_entry.set_text(self.fact.activity)
self.category_entry.set_text(self.fact.category)
self.description_buffer.set_text(self.fact.description)
self.tags_entry.set_tags(self.fact.tags)
self.validate_fields()

def update_status(self, status, markup):
Expand All @@ -166,7 +315,7 @@ def validate_fields(self):

Return the consolidated fact if successful, or None.
"""
fact = self.localized_fact()
fact = self.fact

now = hamster_now()
self.get_widget("button-next-day").set_sensitive(self.date < now.date())
Expand All @@ -187,25 +336,6 @@ def validate_fields(self):
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("""\
Expand Down Expand Up @@ -239,8 +369,18 @@ def on_cancel_clicked(self, button):
def on_close(self, widget, event):
self.close_window()

def on_save_button_clicked(self, button):
if self.fact_id:
runtime.storage.update_fact(self.fact_id, self.fact)
else:
runtime.storage.add_fact(self.fact)
self.close_window()

def on_window_key_pressed(self, tree, event_key):
popups = self.activity.popup.get_property("visible");
popups = (self.cmdline.popup.get_property("visible")
or self.start_time.popup.get_property("visible")
or self.end_time.popup.get_property("visible")
or self.tags_entry.popup.get_property("visible"))

if (event_key.keyval == gdk.KEY_Escape or \
(event_key.keyval == gdk.KEY_w and event_key.state & gdk.ModifierType.CONTROL_MASK)):
Expand All @@ -254,7 +394,8 @@ def on_window_key_pressed(self, tree, event_key):
return False
if self.description_box.has_focus():
return False
self.on_save_button_clicked(None)
if self.validate_fields():
self.on_save_button_clicked(None)

def close_window(self):
if not self.parent:
Expand Down
4 changes: 3 additions & 1 deletion src/hamster/lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ def serialized(self, prepend_date=True):
"""Return a string fully representing the fact."""
name = self.serialized_name()
datetime = self.serialized_time(prepend_date)
return "%s %s" % (datetime, name)
# no need for space if name or datetime is missing
space = " " if name and datetime else ""
return "{}{}{}".format(datetime, space, name)

def _set(self, **kwds):
"""Modify attributes.
Expand Down
Loading