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