-
Notifications
You must be signed in to change notification settings - Fork 17
[#136] Add 'recent activities' to tracking screen #208
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
base: develop
Are you sure you want to change the base?
Changes from 16 commits
54fcc75
975623e
334753c
4c5b85e
5c17e7a
d941aab
8593902
c08bd74
bec62d8
ddafbd1
fa93742
5b16ae9
bd39965
9ccdc80
09aaac6
95fc765
c0dd639
2177d68
4b2819b
90dbcc7
fa8057d
72af08c
d685bf2
c510594
1df269a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,8 +22,10 @@ | |
| from __future__ import absolute_import, unicode_literals | ||
|
|
||
| import datetime | ||
| import operator | ||
| import re | ||
|
|
||
| from orderedset import OrderedSet | ||
| import six | ||
| from six import text_type | ||
|
|
||
|
|
@@ -88,6 +90,8 @@ def clear_children(widget): | |
| It seems GTK really does not have this build in. Iterating over all | ||
| seems a bit blunt, but seems to be the way to do this. | ||
| """ | ||
| # [TODO] | ||
| # Replace with Gtk.Container.foreach()? | ||
| for child in widget.get_children(): | ||
| child.destroy() | ||
| return widget | ||
|
|
@@ -177,6 +181,30 @@ def decompose_raw_fact_string(text, raw=False): | |
| return result | ||
|
|
||
|
|
||
| # [TODO] | ||
| # Oncec LIB-251 has been fixed this should no longer be needed. | ||
| def get_recent_activities(controller, start, end): | ||
| """Return a list of all activities logged in facts within the given timeframe.""" | ||
| # [FIXME] | ||
| # This manual sorting within python is of cause less than optimal. We stick | ||
|
||
| # with it for now as this is just a preliminary workaround helper anyway and | ||
| # effective sorting will need to be implemented by the storage backend in | ||
| # ``hamster-lib``. | ||
| facts = sorted(controller.facts.get_all(start=start, end=end), | ||
| key=operator.attrgetter('start'), reverse=True) | ||
| recent_activities = [fact.activity for fact in facts] | ||
| return OrderedSet(recent_activities) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The activities are returned in a convoluted order. It should be reversed prior to removing duplicates.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure I do understand. Could you elaborate please?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the clarification. To be honest, right now |
||
|
|
||
|
|
||
| def serialize_activity(activity): | ||
|
||
| """Provide a serialized string version of an activity.""" | ||
| if activity.category: | ||
| result = '{a.name}@{a.category.name}'.format(a=activity) | ||
| else: | ||
| result = activity.name | ||
| return text_type(result) | ||
|
|
||
|
|
||
| def get_delta_string(delta): | ||
| """ | ||
| Return a human readable representation of ``datetime.timedelta`` instance. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -169,6 +169,7 @@ def __init__(self, app, *args, **kwargs): | |
| spacing=10, *args, **kwargs) | ||
| self._app = app | ||
| self.set_homogeneous(False) | ||
| self._app.controller.signal_handler.connect('config-changed', self._on_config_changed) | ||
|
|
||
| # [FIXME] | ||
| # Refactor to call separate 'get_widget' methods instead. | ||
|
|
@@ -186,8 +187,16 @@ def __init__(self, app, *args, **kwargs): | |
| # Buttons | ||
| start_button = Gtk.Button(label=_("Start Tracking")) | ||
| start_button.connect('clicked', self._on_start_tracking_button) | ||
| self.start_button = start_button | ||
| self.pack_start(start_button, False, False, 0) | ||
|
|
||
| # Recent activities | ||
| if self._app.config['tracking_show_recent_activities']: | ||
| self.recent_activities_widget = self._get_recent_activities_widget() | ||
| self.pack_start(self.recent_activities_widget, True, True, 0) | ||
| else: | ||
| self.recent_activities_widget = None | ||
|
|
||
| def _start_ongoing_fact(self): | ||
| """ | ||
| Start a new *ongoing fact*. | ||
|
|
@@ -230,6 +239,29 @@ def reset(self): | |
| """Clear all data entry fields.""" | ||
| self.raw_fact_entry.props.text = '' | ||
|
|
||
| def set_raw_fact(self, raw_fact): | ||
| """Set the text in the raw fact entry.""" | ||
| self.raw_fact_entry.props.text = raw_fact | ||
|
|
||
| def _get_recent_activities_widget(self): | ||
| scrolled_window = Gtk.ScrolledWindow() | ||
| scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) | ||
| grid = RecentActivitiesGrid(self, self._app.controller) | ||
| # We need to 'show' the grid early in order to make sure space is | ||
| # allocated to its children so they actually have a height that we can | ||
| # use. | ||
| grid.show_all() | ||
| # We fetch an arbitrary Button as height-reference | ||
|
||
| min_height = 0 | ||
| children = grid.get_children() | ||
| if children: | ||
| height = children[1].get_preferred_height()[1] | ||
| min_height = self._app.config['tracking_recent_activities_items'] * height | ||
|
|
||
| scrolled_window.set_min_content_height(min_height) | ||
| scrolled_window.add(grid) | ||
| return scrolled_window | ||
|
|
||
| # Callbacks | ||
| def _on_start_tracking_button(self, button): | ||
| """Callback for the 'start tracking' button.""" | ||
|
|
@@ -238,3 +270,119 @@ def _on_start_tracking_button(self, button): | |
| def _on_raw_fact_entry_activate(self, evt): | ||
| """Callback for when ``enter`` is pressed within the entry.""" | ||
| self._start_ongoing_fact() | ||
|
|
||
| def _on_config_changed(self, sender): | ||
| """Callback triggered when 'config-changed' event fired.""" | ||
| if self._app.config['tracking_show_recent_activities']: | ||
| # We re-create it even if one existed before because its parameters | ||
| # (e.g. size) may have changed. | ||
| if self.recent_activities_widget: | ||
| self.recent_activities_widget.destroy() | ||
| self.recent_activities_widget = self._get_recent_activities_widget() | ||
| self.pack_start(self.recent_activities_widget, True, True, 0) | ||
| else: | ||
| if self.recent_activities_widget: | ||
| self.recent_activities_widget.destroy() | ||
| self.recent_activities_widget = None | ||
| self.show_all() | ||
|
|
||
|
|
||
| class RecentActivitiesGrid(Gtk.Grid): | ||
| """A widget that lists recent activities and allows for quick continued tracking.""" | ||
|
|
||
| def __init__(self, start_tracking_widget, controller, *args, **kwargs): | ||
| """ | ||
| Initiate widget. | ||
|
|
||
| Args: | ||
| start_tracking_widget (StartTrackingBox): Is needed in order to set the raw fact. | ||
| controller: Is needed in order to query for recent activities. | ||
| """ | ||
| super(Gtk.Grid, self).__init__(*args, **kwargs) | ||
| self._start_tracking_widget = start_tracking_widget | ||
| self._controller = controller | ||
|
|
||
| self._controller.signal_handler.connect('facts-changed', self.refresh) | ||
| self._populate() | ||
|
|
||
| def refresh(self, sender=None): | ||
| """Clear the current content and re-populate and re-draw the widget.""" | ||
| helpers.clear_children(self) | ||
| self._populate() | ||
| self.show_all() | ||
|
|
||
| def _populate(self): | ||
| """Fill the widget with rows per activity.""" | ||
| def add_row_widgets(row_index, activity): | ||
| """ | ||
| Add a set of widgets to a specific row based on the activity passed. | ||
|
|
||
| Args: | ||
| row_counter (int): Which row to add to. | ||
| activity (hamster_lib.Activity): The activity that is represented by this row. | ||
| """ | ||
| def get_label(activity): | ||
| """Label representing the activity/category combination.""" | ||
| label = Gtk.Label(helpers.serialize_activity(activity)) | ||
| label.set_halign(Gtk.Align.START) | ||
| return label | ||
|
|
||
| def get_copy_button(activity): | ||
| """ | ||
| A button that will copy the activity/category string to the raw fact entry. | ||
|
|
||
| The main use case for this is a user that want to add a description or tag before | ||
| actually starting the tracking. | ||
| """ | ||
| button = Gtk.Button('Copy') | ||
| activity = helpers.serialize_activity(activity) | ||
| button.connect('clicked', self._on_copy_button, activity) | ||
| return button | ||
|
|
||
| def get_start_button(activity): | ||
| """A button that will start a new ongoing fact based on that activity.""" | ||
| button = Gtk.Button('Start') | ||
| activity = helpers.serialize_activity(activity) | ||
| button.connect('clicked', self._on_start_button, activity) | ||
| return button | ||
|
|
||
| self.attach(get_label(activity), 0, row_index, 1, 1) | ||
| self.attach(get_copy_button(activity), 1, row_index, 1, 1) | ||
| self.attach(get_start_button(activity), 2, row_index, 1, 1) | ||
|
|
||
| today = datetime.date.today() | ||
| start = today - datetime.timedelta(1) | ||
| activities = helpers.get_recent_activities(self._controller, start, today) | ||
|
|
||
| row_index = 0 | ||
| for activity in activities: | ||
| add_row_widgets(row_index, activity) | ||
| row_index += 1 | ||
|
|
||
| def _on_copy_button(self, button, activity): | ||
| """ | ||
| Set the activity/category text in the 'start tracking entry'. | ||
|
|
||
| Args: | ||
| button (Gtk.Button): The button that was clicked. | ||
| activity (text_type): Activity text to be copied as raw fact. | ||
|
|
||
| Note: | ||
| Besides copying the text we also assign focus and place the cursor | ||
| at the end of the pasted text as to facilitate fast entry of | ||
| additional text. | ||
| """ | ||
| self._start_tracking_widget.set_raw_fact(activity) | ||
| self._start_tracking_widget.raw_fact_entry.grab_focus_without_selecting() | ||
| self._start_tracking_widget.raw_fact_entry.set_position(len(activity)) | ||
|
|
||
| def _on_start_button(self, button, activity): | ||
| """ | ||
| Start a new ongoing fact based on this activity/category. | ||
|
|
||
| Args: | ||
| button (Gtk.Button): The button that was clicked. | ||
| activity (text_type): Activity text to be copied as raw fact. | ||
| """ | ||
| self._start_tracking_widget.set_raw_fact(activity) | ||
| self._start_tracking_widget._start_ongoing_fact() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use
countornumber.itemsis less clear.