Oops, added a bit too much
[doneit] / src / gtk_rtmilk.py
1 import webbrowser
2 import datetime
3 import urlparse
4
5 import gobject
6 import gtk
7
8 import toolbox
9 import gtk_toolbox
10 import rtmilk
11 import rtmapi
12
13
14 def abbreviate(text, expectedLen):
15         singleLine = " ".join(text.split("\n"))
16         lineLen = len(singleLine)
17         if lineLen <= expectedLen:
18                 return singleLine
19
20         abbrev = "..."
21
22         leftLen = expectedLen // 2 - 1
23         rightLen = max(expectedLen - leftLen - len(abbrev) + 1, 1)
24
25         abbrevText =  singleLine[0:leftLen] + abbrev + singleLine[-rightLen:-1]
26         assert len(abbrevText) <= expectedLen, "Too long: '%s'" % abbrevText
27         return abbrevText
28
29
30 def abbreviate_url(url, domainLength, pathLength):
31         urlParts = urlparse.urlparse(url)
32
33         netloc = urlParts.netloc
34         path = urlParts.path
35
36         pathLength += max(domainLength - len(netloc), 0)
37         domainLength += max(pathLength - len(path), 0)
38
39         netloc = abbreviate(netloc, domainLength)
40         path = abbreviate(path, pathLength)
41         return netloc + path
42
43
44 def get_token(username, apiKey, secret):
45         token = None
46         rtm = rtmapi.RTMapi(username, apiKey, secret, token)
47
48         authURL = rtm.getAuthURL()
49         webbrowser.open(authURL)
50         mb = gtk_toolbox.MessageBox2("You need to authorize DoneIt with\nRemember The Milk.\nClick OK after you authorize.")
51         mb.run()
52
53         token = rtm.getToken()
54         return token
55
56
57 class GtkRtMilk(object):
58
59         ID_IDX = 0
60         COMPLETION_IDX = 1
61         NAME_IDX = 2
62         PRIORITY_IDX = 3
63         DUE_IDX = 4
64         FUZZY_IDX = 5
65         LINK_IDX = 6
66         NOTES_IDX = 7
67
68         def __init__(self, widgetTree):
69                 """
70                 @note Thread agnostic
71                 """
72                 self._clipboard = gtk.clipboard_get()
73
74                 self._showCompleted = False
75                 self._showIncomplete = True
76
77                 self._editDialog = gtk_toolbox.EditTaskDialog(widgetTree)
78
79                 self._projectsList = gtk.ListStore(gobject.TYPE_STRING)
80                 self._projectsCombo = widgetTree.get_widget("projectsCombo")
81                 self._onListActivateId = 0
82
83                 self._itemList = gtk.ListStore(
84                         gobject.TYPE_STRING, # id
85                         gobject.TYPE_BOOLEAN,   # is complete
86                         gobject.TYPE_STRING, # name
87                         gobject.TYPE_STRING, # priority
88                         gobject.TYPE_STRING, # due
89                         gobject.TYPE_STRING, # fuzzy due
90                         gobject.TYPE_STRING, # Link
91                         gobject.TYPE_STRING, # Notes
92                 )
93                 self._completionColumn = gtk.TreeViewColumn('') # Complete?
94                 self._completionCell = gtk.CellRendererToggle()
95                 self._completionCell.set_property("activatable", True)
96                 self._completionCell.connect("toggled", self._on_completion_change)
97                 self._completionColumn.pack_start(self._completionCell, False)
98                 self._completionColumn.set_attributes(self._completionCell, active=self.COMPLETION_IDX)
99                 self._priorityColumn = gtk.TreeViewColumn('') # Priority
100                 self._priorityCell = gtk.CellRendererText()
101                 self._priorityColumn.pack_start(self._priorityCell, False)
102                 self._priorityColumn.set_attributes(self._priorityCell, text=self.PRIORITY_IDX)
103                 self._nameColumn = gtk.TreeViewColumn('Name')
104                 self._nameCell = gtk.CellRendererText()
105                 self._nameColumn.pack_start(self._nameCell, True)
106                 self._nameColumn.set_attributes(self._nameCell, text=self.NAME_IDX)
107                 self._dueColumn = gtk.TreeViewColumn('Due')
108                 self._dueCell = gtk.CellRendererText()
109                 self._dueColumn.pack_start(self._nameCell, False)
110                 self._dueColumn.set_attributes(self._nameCell, text=self.DUE_IDX)
111                 self._linkColumn = gtk.TreeViewColumn('') # Link
112                 self._linkCell = gtk.CellRendererText()
113                 self._linkColumn.pack_start(self._nameCell, False)
114                 self._linkColumn.set_attributes(self._nameCell, text=self.LINK_IDX)
115                 self._notesColumn = gtk.TreeViewColumn('') # Notes
116                 self._notesCell = gtk.CellRendererText()
117                 self._notesColumn.pack_start(self._nameCell, False)
118                 self._notesColumn.set_attributes(self._nameCell, text=self.NOTES_IDX)
119
120                 self._todoItemTree = widgetTree.get_widget("todoItemTree")
121                 self._onItemSelectId = 0
122
123                 self._taskNameEntry = widgetTree.get_widget("add-taskNameEntry")
124                 self._addTaskButton = widgetTree.get_widget("add-addTaskButton")
125                 self._pasteTaskNameButton = widgetTree.get_widget("add-pasteTaskNameButton")
126                 self._clearTaskNameButton = widgetTree.get_widget("add-clearTaskNameButton")
127                 self._onAddId = None
128                 self._onAddClickedId = None
129                 self._onAddReleasedId = None
130                 self._addToEditTimerId = None
131                 self._onClearId = None
132                 self._onPasteId = None
133
134                 self._credentials = gtk_toolbox.LoginWindow(widgetTree)
135                 self._manager = None
136
137         @staticmethod
138         def name():
139                 return "Remember The Milk"
140
141         def start_session(self):
142                 username, password = self._credentials.request_credentials()
143                 token = get_token(username, rtmilk.RtMilkManager.API_KEY, rtmilk.RtMilkManager.SECRET)
144                 return rtmilk.RtMilkManager(username, password, token)
145
146         def enable(self):
147                 """
148                 @note UI Thread
149                 """
150                 self._manager = self.start_session()
151
152                 self._projectsList.clear()
153                 self._populate_projects()
154
155                 self._itemList.clear()
156                 self._todoItemTree.append_column(self._completionColumn)
157                 self._todoItemTree.append_column(self._priorityColumn)
158                 self._todoItemTree.append_column(self._nameColumn)
159                 self._todoItemTree.append_column(self._dueColumn)
160                 self._todoItemTree.append_column(self._linkColumn)
161                 self._todoItemTree.append_column(self._notesColumn)
162                 self._reset_task_list()
163
164                 self._todoItemTree.set_headers_visible(False)
165                 self._nameColumn.set_expand(True)
166
167                 self._editDialog.enable(self._manager)
168
169                 self._onListActivateId = self._projectsCombo.connect("changed", self._on_list_activate)
170                 self._onItemSelectId = self._todoItemTree.connect("row-activated", self._on_item_select)
171                 self._onAddId = self._addTaskButton.connect("clicked", self._on_add)
172                 self._onAddClickedId = self._addTaskButton.connect("pressed", self._on_add_pressed)
173                 self._onAddReleasedId = self._addTaskButton.connect("released", self._on_add_released)
174                 self._onPasteId = self._pasteTaskNameButton.connect("clicked", self._on_paste)
175                 self._onClearId = self._clearTaskNameButton.connect("clicked", self._on_clear)
176
177         def disable(self):
178                 """
179                 @note UI Thread
180                 """
181                 self._projectsCombo.disconnect(self._onListActivateId)
182                 self._todoItemTree.disconnect(self._onItemSelectId)
183                 self._addTaskButton.disconnect(self._onAddId)
184                 self._addTaskButton.disconnect(self._onAddClickedId)
185                 self._addTaskButton.disconnect(self._onAddReleasedId)
186                 self._pasteTaskNameButton.disconnect(self._onPasteId)
187                 self._clearTaskNameButton.disconnect(self._onClearId)
188
189                 self._editDialog.disable()
190
191                 self._projectsList.clear()
192                 self._projectsCombo.set_model(None)
193                 self._projectsCombo.disconnect("changed", self._on_list_activate)
194
195                 self._todoItemTree.remove_column(self._completionColumn)
196                 self._todoItemTree.remove_column(self._priorityColumn)
197                 self._todoItemTree.remove_column(self._nameColumn)
198                 self._todoItemTree.remove_column(self._dueColumn)
199                 self._todoItemTree.remove_column(self._linkColumn)
200                 self._todoItemTree.remove_column(self._notesColumn)
201                 self._itemList.clear()
202                 self._itemList.set_model(None)
203
204                 self._manager = None
205
206         def _populate_projects(self):
207                 for project in self._manager.get_projects():
208                         projectName = project["name"]
209                         isVisible = project["isVisible"]
210                         row = (projectName, )
211                         if isVisible:
212                                 self._projectsList.append(row)
213                 self._projectsCombo.set_model(self._projectsList)
214                 cell = gtk.CellRendererText()
215                 self._projectsCombo.pack_start(cell, True)
216                 self._projectsCombo.add_attribute(cell, 'text', 0)
217                 self._projectsCombo.set_active(0)
218
219         def _reset_task_list(self):
220                 currentProject = self._get_project()
221                 projId = self._manager.lookup_project(currentProject)["id"]
222                 isMeta = self._manager.get_project(projId)["isMeta"]
223                 # @todo RTM handles this by defaulting to a specific list
224                 self._addTaskButton.set_sensitive(not isMeta)
225
226                 self._itemList.clear()
227                 self._populate_items()
228
229         def _get_project(self):
230                 currentProjectName = self._projectsCombo.get_active_text()
231                 return currentProjectName
232
233         def _populate_items(self):
234                 currentProject = self._get_project()
235                 projId = self._manager.lookup_project(currentProject)["id"]
236                 for taskDetails in self._manager.get_tasks_with_details(projId):
237                         show = self._showCompleted if taskDetails["isCompleted"] else self._showIncomplete
238                         if not show:
239                                 continue
240                         id = taskDetails["id"]
241                         isCompleted = taskDetails["isCompleted"]
242                         name = abbreviate(taskDetails["name"], 100)
243                         priority = taskDetails["priority"]
244                         dueDescription = taskDetails["dueDate"]
245                         if dueDescription:
246                                 dueDate = datetime.datetime.strptime(dueDescription, "%Y-%m-%dT%H:%M:%SZ")
247                                 fuzzyDue = toolbox.to_fuzzy_date(dueDate)
248                         else:
249                                 fuzzyDue = ""
250
251                         linkDisplay = taskDetails["url"]
252                         linkDisplay = abbreviate_url(linkDisplay, 20, 10)
253
254                         notes = taskDetails["notes"]
255                         notesDisplay = "%d Notes" % len(notes) if notes else ""
256
257                         row = (id, isCompleted, name, priority, dueDescription, fuzzyDue, linkDisplay, notesDisplay)
258                         self._itemList.append(row)
259                 self._todoItemTree.set_model(self._itemList)
260
261         def _on_list_activate(self, *args):
262                 self._reset_task_list()
263
264         def _on_item_select(self, treeView, path, viewColumn):
265                 taskId = self._itemList[path[0]][self.ID_IDX]
266
267                 if viewColumn is self._priorityColumn:
268                         pass
269                 elif viewColumn is self._nameColumn:
270                         self._editDialog.request_task(self._manager, taskId)
271                         self._reset_task_list()
272                 elif viewColumn is self._dueColumn:
273                         self._editDialog.request_task(self._manager, taskId)
274                         self._reset_task_list()
275                 elif viewColumn is self._linkColumn:
276                         webbrowser.open(self._manager.get_task_details(taskId)["url"])
277                 elif viewColumn is self._notesColumn:
278                         pass
279
280         def _on_add(self, *args):
281                 name = self._taskNameEntry.get_text()
282
283                 currentProject = self._get_project()
284                 projId = self._manager.lookup_project(currentProject)["id"]
285                 taskId = self._manager.add_task(projId, name)
286
287                 self._taskNameEntry.set_text("")
288                 self._reset_task_list()
289
290         def _on_add_edit(self, *args):
291                 name = self._taskNameEntry.get_text()
292
293                 currentProject = self._get_project()
294                 projId = self._manager.lookup_project(currentProject)["id"]
295                 taskId = self._manager.add_task(projId, name)
296
297                 try:
298                         self._editDialog.request_task(self._manager, taskId)
299                 finally:
300                         self._taskNameEntry.set_text("")
301                         self._reset_task_list()
302
303         def _on_add_pressed(self, widget):
304                 self._addToEditTimerId = gobject.timeout_add(1000, self._on_add_edit)
305
306         def _on_add_released(self, widget):
307                 if self._addToEditTimerId is not None:
308                         gobject.source_remove(self._addToEditTimerId)
309                 self._addToEditTimerId = None
310
311         def _on_paste(self, *args):
312                 entry = self._taskNameEntry.get_text()
313                 entry += self._clipboard.wait_for_text()
314                 self._taskNameEntry.set_text(entry)
315
316         def _on_clear(self, *args):
317                 self._taskNameEntry.set_text("")
318
319         def _on_completion_change(self, cell, path):
320                 taskId = self._itemList[path[0]][self.ID_IDX]
321                 self._manager.complete_task(taskId)
322                 self._reset_task_list()