1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Gettings Things Gnome! - a personal organizer for the GNOME desktop
4
# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
6
# This program is free software: you can redistribute it and/or modify it under
7
# the terms of the GNU General Public License as published by the Free Software
8
# Foundation, either version 3 of the License, or (at your option) any later
11
# This program is distributed in the hope that it will be useful, but WITHOUT
12
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16
# You should have received a copy of the GNU General Public License along with
17
# this program. If not, see <http://www.gnu.org/licenses/>.
18
# -----------------------------------------------------------------------------
20
#This is the TaskEditor
22
#It's the window you see when you double-click on a Task
23
#The main text widget is a home-made TextView called TaskView (see taskview.py)
24
#The rest is the logic of the widget : date changing widgets, buttons, ...
29
from GTG import ngettext
30
from GTG import PLUGIN_DIR
31
from GTG import DATA_DIR
32
from GTG.taskeditor import GnomeConfig
33
from GTG.tools import dates
34
from GTG.taskeditor.taskview import TaskView
35
from GTG.core.plugins.engine import PluginEngine
36
from GTG.core.plugins.api import PluginAPI
37
from GTG.core.task import Task
41
except: # pylint: disable-msg=W0702
46
except: # pylint: disable-msg=W0702
52
#delete_callback is the function called on deletion
53
#close_callback is the function called on close
54
#opentask_callback is the function to open a new editor
55
#tasktitle_callback is called when title changes
56
#notes is experimental (bool)
57
#taskconfig is a ConfigObj dic to save infos about tasks
58
#thisisnew is True when a new task is created and opened
59
def __init__(self, requester, task, plugins, taskbrowser,
60
delete_callback=None, close_callback=None,opentask_callback=None, \
61
tasktitle_callback=None, notes=False,taskconfig=None,\
62
plugin_apis=None,thisisnew=False, clipboard=None) :
64
self.config = taskconfig
65
self.p_apis = plugin_apis
66
self.pengine = taskbrowser.pengine
68
self.taskbrowser = taskbrowser
69
self.clipboard = clipboard
70
self.builder = gtk.Builder()
71
self.builder.add_from_file(GnomeConfig.GLADE_FILE)
72
self.donebutton = self.builder.get_object("mark_as_done_editor")
73
self.dismissbutton = self.builder.get_object("dismiss_editor")
74
self.deletebutton = self.builder.get_object("delete_editor")
75
self.deletebutton.set_tooltip_text(GnomeConfig.DELETE_TOOLTIP)
76
self.subtask_button = self.builder.get_object("insert_subtask")
77
self.subtask_button.set_tooltip_text(GnomeConfig.SUBTASK_TOOLTIP)
78
self.inserttag_button = self.builder.get_object("inserttag")
79
self.inserttag_button.set_tooltip_text(GnomeConfig.TAG_TOOLTIP)
80
#Create our dictionary and connect it
82
"mark_as_done_clicked" : self.change_status,
83
"on_dismiss" : self.dismiss,
84
"on_keepnote_clicked" : self.keepnote,
85
"delete_clicked" : self.delete_task,
86
"on_duedate_pressed" : (self.on_date_pressed,"due"),
87
"on_startdate_pressed" : (self.on_date_pressed,"start"),
88
"on_closeddate_pressed" : (self.on_date_pressed, "closed"),
89
"close_clicked" : self.close,
90
"startingdate_changed" : (self.date_changed,"start"),
91
"duedate_changed" : (self.date_changed,"due"),
92
"closeddate_changed" : (self.date_changed, "closed"),
93
"on_insert_subtask_clicked" : self.insert_subtask,
94
"on_inserttag_clicked" : self.inserttag_clicked,
95
"on_move" : self.on_move,
96
"on_nodate" : self.nodate_pressed,
97
"on_set_fuzzydate_now" : self.set_fuzzydate_now,
98
"on_set_fuzzydate_soon" : self.set_fuzzydate_soon,
99
"on_set_fuzzydate_later": self.set_fuzzydate_later,
101
self.builder.connect_signals(dic)
102
self.window = self.builder.get_object("TaskEditor")
103
#Removing the Normal textview to replace it by our own
104
#So don't try to change anything with glade, this is a home-made widget
105
textview = self.builder.get_object("textview")
106
scrolled = self.builder.get_object("scrolledtask")
107
scrolled.remove(textview)
108
self.open_task = opentask_callback
109
self.task_title = tasktitle_callback
110
self.textview = TaskView(self.req,self.clipboard)
112
self.textview.set_subtask_callback(self.new_subtask)
113
self.textview.open_task_callback(self.open_task)
114
self.textview.tasktitle_callback(self.task_title)
115
self.textview.set_left_margin(7)
116
self.textview.set_right_margin(5)
117
scrolled.add(self.textview)
119
self.calendar = self.builder.get_object("calendar")
120
self.cal_widget = self.builder.get_object("calendar1")
121
self.calendar_fuzzydate_btns = self.builder.get_object("fuzzydate_btns")
122
#self.cal_widget.set_property("no-month-change",True)
124
self.sigid_month = None
125
#Do we have to close the calendar when date is changed ?
126
#This is a ugly hack to close the calendar on the first click
127
self.close_when_changed = True
128
self.duedate_widget = self.builder.get_object("duedate_entry")
129
self.startdate_widget = self.builder.get_object("startdate_entry")
130
self.closeddate_widget = self.builder.get_object("closeddate_entry")
131
self.dayleft_label = self.builder.get_object("dayleft")
132
self.tasksidebar = self.builder.get_object("tasksidebar")
133
self.keepnote_button = self.builder.get_object("keepnote")
135
self.keepnote_button.hide()
136
separator = self.builder.get_object("separator_note")
138
#We will keep the name of the opened calendar
139
#Empty means that no calendar is opened
140
self.__opened_date = ''
142
# Define accelerator keys
143
self.init_accelerators()
146
tags = task.get_tags()
147
self.textview.subtasks_callback(task.get_subtask_tids)
148
self.textview.removesubtask_callback(task.remove_subtask)
149
self.textview.set_get_tagslist_callback(task.get_tags_name)
150
self.textview.set_add_tag_callback(task.add_tag)
151
self.textview.set_remove_tag_callback(task.remove_tag)
152
self.textview.save_task_callback(self.light_save)
153
self.delete = delete_callback
154
self.closing = close_callback
156
texte = self.task.get_text()
157
title = self.task.get_title()
158
#the first line is the title
159
self.textview.set_text("%s\n"%title)
160
#we insert the rest of the task
162
self.textview.insert("%s"%texte)
164
#If not text, we insert tags
167
self.textview.insert_text("%s, "%t.get_name())
168
self.textview.insert_text("\n")
169
#If we don't have text, we still need to insert subtasks if any
170
subtasks = task.get_subtask_tids()
172
self.textview.insert_subtasks(subtasks)
173
#We select the title if it's a new task
175
self.textview.select_title()
177
self.task.set_to_keep()
178
self.textview.modified(full=True)
179
self.window.connect("destroy", self.destruction)
182
self.plugins = plugins
183
self.te_plugin_api = PluginAPI(window = self.window,
186
builder = self.builder,
187
requester = self.req,
189
task_modelsort = None,
191
ctask_modelsort = None,
196
browser = self.taskbrowser,
198
self.p_apis.append(self.te_plugin_api)
199
self.pengine.onTaskLoad(self.te_plugin_api)
201
#Putting the refresh callback at the end make the start a lot faster
202
self.textview.refresh_callback(self.refresh_editor)
203
self.refresh_editor()
204
self.textview.grab_focus()
206
#restoring size and position, spatial tasks
208
tid = self.task.get_id()
209
if tid in self.config:
210
if "position" in self.config[tid]:
211
pos = self.config[tid]["position"]
212
self.move(pos[0],pos[1])
213
#print "restoring position %s %s" %(pos[0],pos[1])
214
if "size" in self.config[tid]:
215
size = self.config[tid]["size"]
216
#print "size %s - %s" %(str(size[0]),str(size[1]))
217
#this eval(str()) is a hack to accept both int and str
218
self.window.resize(eval(str(size[0])),eval(str(size[1])))
221
self.textview.set_editable(True)
223
# Define accelerator-keys for this dialog
225
def init_accelerators(self):
226
agr = gtk.AccelGroup()
227
self.window.add_accel_group(agr)
229
# Escape and Ctrl-W close the dialog. It's faster to call close
230
# directly, rather than use the close button widget
231
key, modifier = gtk.accelerator_parse('Escape')
232
agr.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self.close)
234
key, modifier = gtk.accelerator_parse('<Control>w')
235
agr.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self.close)
237
# Ctrl-N creates a new task
238
key, modifier = gtk.accelerator_parse('<Control>n')
239
agr.connect_group(key, modifier, gtk.ACCEL_VISIBLE, self.new_task)
241
# Ctrl-Shift-N creates a new subtask
242
insert_subtask = self.builder.get_object("insert_subtask")
243
key, mod = gtk.accelerator_parse("<Control><Shift>n")
244
insert_subtask.add_accelerator('clicked', agr, key, mod, gtk.ACCEL_VISIBLE)
246
# Ctrl-D marks task as done
247
mark_as_done_editor = self.builder.get_object('mark_as_done_editor')
248
key, mod = gtk.accelerator_parse('<Control>d')
249
mark_as_done_editor.add_accelerator('clicked', agr, key, mod, gtk.ACCEL_VISIBLE)
251
# Ctrl-I marks task as dismissed
252
dismiss_editor = self.builder.get_object('dismiss_editor')
253
key, mod = gtk.accelerator_parse('<Control>i')
254
dismiss_editor.add_accelerator('clicked', agr, key, mod, gtk.ACCEL_VISIBLE)
257
#Can be called at any time to reflect the status of the Task
258
#Refresh should never interfere with the TaskView.
259
#If a title is passed as a parameter, it will become
260
#the new window title. If not, we will look for the task title.
261
#Refreshtext is whether or not we should refresh the TaskView
262
#(doing it all the time is dangerous if the task is empty)
263
def refresh_editor(self, title=None, refreshtext=False):
267
self.window.set_title(title)
270
self.window.set_title(self.task.get_title())
272
status = self.task.get_status()
273
if status == Task.STA_DISMISSED:
274
self.donebutton.set_label(GnomeConfig.MARK_DONE)
275
self.donebutton.set_tooltip_text(GnomeConfig.MARK_DONE_TOOLTIP)
276
self.donebutton.set_icon_name("gtg-task-done")
277
self.dismissbutton.set_label(GnomeConfig.MARK_UNDISMISS)
278
self.dismissbutton.set_tooltip_text(GnomeConfig.MARK_UNDISMISS_TOOLTIP)
279
self.dismissbutton.set_icon_name("gtg-task-undismiss")
280
elif status == Task.STA_DONE:
281
self.donebutton.set_label(GnomeConfig.MARK_UNDONE)
282
self.donebutton.set_tooltip_text(GnomeConfig.MARK_UNDONE_TOOLTIP)
283
self.donebutton.set_icon_name("gtg-task-undone")
284
self.dismissbutton.set_label(GnomeConfig.MARK_DISMISS)
285
self.dismissbutton.set_tooltip_text(GnomeConfig.MARK_DISMISS_TOOLTIP)
286
self.dismissbutton.set_icon_name("gtg-task-dismiss")
288
self.donebutton.set_label(GnomeConfig.MARK_DONE)
289
self.donebutton.set_tooltip_text(GnomeConfig.MARK_DONE_TOOLTIP)
290
self.donebutton.set_icon_name("gtg-task-done")
291
self.dismissbutton.set_label(GnomeConfig.MARK_DISMISS)
292
self.dismissbutton.set_tooltip_text(GnomeConfig.MARK_DISMISS_TOOLTIP)
293
self.dismissbutton.set_icon_name("gtg-task-dismiss")
296
self.donebutton.hide()
297
self.tasksidebar.hide()
298
self.keepnote_button.set_label(GnomeConfig.MAKE_TASK)
300
self.donebutton.show()
301
self.tasksidebar.show()
302
self.keepnote_button.set_label(GnomeConfig.KEEP_NOTE)
304
#Refreshing the status bar labels and date boxes
305
if status in [Task.STA_DISMISSED, Task.STA_DONE]:
306
self.builder.get_object("label2").hide()
307
self.builder.get_object("hbox1").hide()
308
self.builder.get_object("label4").show()
309
self.builder.get_object("hbox4").show()
311
self.builder.get_object("label4").hide()
312
self.builder.get_object("hbox4").hide()
313
self.builder.get_object("label2").show()
314
self.builder.get_object("hbox1").show()
316
#refreshing the due date field
317
duedate = self.task.get_due_date()
318
prevdate = dates.strtodate(self.duedate_widget.get_text())
319
if duedate != prevdate or type(duedate) is not type(prevdate):
320
zedate = str(duedate).replace("-", date_separator)
321
self.duedate_widget.set_text(zedate)
322
# refreshing the closed date field
323
closeddate = self.task.get_closed_date()
324
prevcldate = dates.strtodate(self.closeddate_widget.get_text())
325
if closeddate != prevcldate or type(closeddate) is not type(prevcldate):
326
zecldate = str(closeddate).replace("-", date_separator)
327
self.closeddate_widget.set_text(zecldate)
328
#refreshing the day left label
329
#If the task is marked as done, we display the delay between the
330
#due date and the actual closing date. If the task isn't marked
331
#as done, we display the number of days left.
332
if status in [Task.STA_DISMISSED, Task.STA_DONE]:
333
delay = self.task.get_days_late()
337
txt = "Completed on time"
339
txt = ngettext("Completed %(days)d day late", "Completed %(days)d days late", delay) % {'days': delay}
341
abs_delay = abs(delay)
342
txt = ngettext("Completed %(days)d day early", "Completed %(days)d days early", abs_delay) % {'days': abs_delay}
344
result = self.task.get_days_left()
348
txt = _("Due tomorrow !")
350
txt = ngettext("%(days)d day left", "%(days)d days left", result) % {'days': result}
352
txt = _("Due today !")
354
txt = _("Due yesterday")
356
abs_result = abs(result)
357
txt = ngettext("Was %(days)d day ago", "Was %(days)d days ago", abs_result) % {'days': abs_result}
358
window_style = self.window.get_style()
359
color = str(window_style.text[gtk.STATE_INSENSITIVE])
360
self.dayleft_label.set_markup("<span color='"+color+"'>"+txt+"</span>")
362
startdate = self.task.get_start_date()
363
prevdate = dates.strtodate(self.startdate_widget.get_text())
364
if startdate != prevdate or type(startdate) is not type(prevdate):
365
zedate = str(startdate).replace("-",date_separator)
366
self.startdate_widget.set_text(zedate)
367
#Refreshing the tag list in the insert tag button
368
taglist = self.req.get_used_tags()
373
if not self.task.has_tags(tag_list=[t]) :
375
mi = gtk.MenuItem(label=tt, use_underline=False)
376
mi.connect("activate",self.inserttag,tt)
380
self.inserttag_button.set_menu(menu)
383
self.textview.modified(refresheditor=False)
388
def date_changed(self,widget,data):
389
text = widget.get_text()
393
datetoset = dates.no_date
395
datetoset = dates.strtodate(text)
400
#If the date is valid, we write with default color in the widget
401
# "none" will set the default color.
402
widget.modify_text(gtk.STATE_NORMAL, None)
403
widget.modify_base(gtk.STATE_NORMAL, None)
405
self.task.set_start_date(datetoset)
407
self.task.set_due_date(datetoset)
408
elif data == "closed" :
409
self.task.set_closed_date(datetoset)
411
#We should write in red in the entry if the date is not valid
412
widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("#F00"))
413
widget.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#F88"))
415
def _mark_today_in_bold(self):
416
today = dates.date_today()
417
#selected is a tuple containing (year, month, day)
418
selected = self.cal_widget.get_date()
419
#the following "-1" is because in pygtk calendar the month is 0-based,
420
# in gtg (and datetime.date) is 1-based.
421
if selected[1] == today.month() - 1 and selected[0] == today.year():
422
self.cal_widget.mark_day(today.day())
424
self.cal_widget.unmark_day(today.day())
427
def on_date_pressed(self, widget,data):
428
"""Called when the due button is clicked."""
430
self.__opened_date = data
431
self._mark_today_in_bold()
432
if self.__opened_date == "due" :
433
toset = self.task.get_due_date()
434
self.calendar_fuzzydate_btns.show()
435
elif self.__opened_date == "start" :
436
toset = self.task.get_start_date()
437
self.calendar_fuzzydate_btns.hide()
438
elif self.__opened_date == "closed" :
439
toset = self.task.get_closed_date()
440
self.calendar_fuzzydate_btns.hide()
442
rect = widget.get_allocation()
443
x, y = widget.window.get_origin()
444
cal_width, cal_height = self.calendar.get_size()
445
self.calendar.move((x + rect.x - cal_width + rect.width)
446
, (y + rect.y - cal_height))
448
"""Because some window managers ignore move before you show a window."""
449
self.calendar.move((x + rect.x - cal_width + rect.width)
450
, (y + rect.y - cal_height))
452
self.calendar.grab_add()
453
#We grab the pointer in the calendar
454
gdk.pointer_grab(self.calendar.window, True,gdk.BUTTON1_MASK|gdk.MOD2_MASK)
455
#we will close the calendar if the user clicks outside
457
if not isinstance(toset, dates.FuzzyDate):
459
# we set the widget to today's date if there is not a date defined
460
toset = dates.date_today()
466
#We have to select the day first. If not, we might ask for
467
#February while still being on 31 -> error !
468
self.cal_widget.select_day(d)
469
self.cal_widget.select_month(int(m)-1,int(y))
471
self.calendar.connect('button-press-event', self.__focus_out)
472
self.sigid = self.cal_widget.connect("day-selected",self.day_selected)
473
self.sigid_month = self.cal_widget.connect("month-changed",self.month_changed)
475
def day_selected(self,widget) :
476
y,m,d = widget.get_date()
477
if self.__opened_date == "due" :
478
self.task.set_due_date(dates.strtodate("%s-%s-%s"%(y,m+1,d)))
479
elif self.__opened_date == "start" :
480
self.task.set_start_date(dates.strtodate("%s-%s-%s"%(y,m+1,d)))
481
elif self.__opened_date == "closed" :
482
self.task.set_closed_date(dates.strtodate("%s-%s-%s"%(y,m+1,d)))
483
if self.close_when_changed :
484
#When we select a day, we connect the mouse release to the
485
#closing of the calendar.
486
self.mouse_sigid = self.cal_widget.connect('event',self.__mouse_release)
488
self.close_when_changed = True
489
self.refresh_editor()
491
def __mouse_release(self,widget,event):
492
if event.type == gtk.gdk.BUTTON_RELEASE:
493
self.__close_calendar()
494
self.cal_widget.disconnect(self.mouse_sigid)
496
def month_changed(self,widget) :
497
#This is a ugly hack to close the calendar on the first click
498
self.close_when_changed = False
499
self._mark_today_in_bold()
501
def set_opened_date(self, date):
502
if self.__opened_date == "due" :
503
self.task.set_due_date(date)
504
elif self.__opened_date == "start" :
505
self.task.set_start_date(date)
506
elif self.__opened_date == "closed" :
507
self.task.set_closed_date(date)
508
self.refresh_editor()
509
self.__close_calendar()
511
def nodate_pressed(self,widget) : #pylint: disable-msg=W0613
512
self.set_opened_date(dates.no_date)
514
def set_fuzzydate_now(self, widget) : #pylint: disable-msg=W0613
515
self.set_opened_date(dates.NOW)
517
def set_fuzzydate_soon(self, widget) : #pylint: disable-msg=W0613
518
self.set_opened_date(dates.SOON)
520
def set_fuzzydate_later(self, widget) : #pylint: disable-msg=W0613
521
self.set_opened_date(dates.LATER)
523
def dismiss(self,widget) : #pylint: disable-msg=W0613
524
stat = self.task.get_status()
525
if stat == "Dismiss":
526
self.task.set_status("Active")
527
self.refresh_editor()
529
self.task.set_status("Dismiss")
532
def keepnote(self,widget) : #pylint: disable-msg=W0613
533
stat = self.task.get_status()
537
self.task.set_status(toset)
538
self.refresh_editor()
540
def change_status(self,widget) : #pylint: disable-msg=W0613
541
stat = self.task.get_status()
543
self.task.set_status("Active")
544
self.refresh_editor()
546
self.task.set_status("Done")
549
def delete_task(self,widget) :
551
result = self.delete(widget,self.task.get_id())
552
#if the task was deleted, we close the window
553
if result : self.window.destroy()
556
#Take the title as argument and return the subtask ID
557
def new_subtask(self,title=None,tid=None) :
559
self.task.add_subtask(tid)
561
subt = self.task.new_subtask()
562
subt.set_title(title)
567
def new_task(self, *args):
568
task = self.req.new_task(tags=None, newtask=True)
569
task_id = task.get_id()
570
self.open_task(task_id)
572
def insert_subtask(self,widget) : #pylint: disable-msg=W0613
573
self.textview.insert_newtask()
574
self.textview.grab_focus()
576
def inserttag_clicked(self,widget) : #pylint: disable-msg=W0613
577
itera = self.textview.get_insert()
578
if itera.starts_line() :
579
self.textview.insert_text("@",itera)
581
self.textview.insert_text(" @",itera)
582
self.textview.grab_focus()
584
def inserttag(self,widget,tag) : #pylint: disable-msg=W0613
585
self.textview.insert_tags([tag])
586
self.textview.grab_focus()
589
self.task.set_title(self.textview.get_title())
590
self.task.set_text(self.textview.get_text())
592
if self.config != None:
594
self.time = time.time()
595
#light_save save the task without refreshing every 30seconds
596
#We will reduce the time when the get_text will be in another thread
597
def light_save(self) :
598
#if self.time is none, we never called any save
600
diff = time.time() - self.time
601
tosave = diff > GnomeConfig.SAVETIME
603
#we don't want to save a task while opening it
604
tosave = self.textview.get_editable()
610
#This will bring the Task Editor to front
612
self.window.present()
617
self.window.move(xx,yy)
620
def get_position(self):
621
return self.window.get_position()
623
def on_move(self,widget,event):
625
if self.config != None:
626
tid = self.task.get_id()
627
if not tid in self.config :
628
self.config[tid] = dict()
629
#print "saving task position %s" %str(self.get_position())
630
self.config[tid]["position"] = self.get_position()
631
self.config[tid]["size"] = self.window.get_size()
633
#We define dummy variable for when close is called from a callback
634
def close(self,window=None,a=None,b=None,c=None) : #pylint: disable-msg=W0613
635
#We should also destroy the whole taskeditor object.
636
self.window.destroy()
638
#The destroy signal is linked to the "close" button. So if we call
639
#destroy in the close function, this will cause the close to be called twice
640
#To solve that, close will just call "destroy" and the destroy signal
641
#Will be linked to this destruction method that will save the task
642
def destruction(self,a=None) :#pylint: disable-msg=W0613
643
#Save should be also called when buffer is modified
644
self.pengine.onTaskClose(self.plugins, self.te_plugin_api)
645
self.p_apis.remove(self.te_plugin_api)
646
tid = self.task.get_id()
647
if self.task.is_new():
648
self.req.delete_task(tid)
651
for i in self.task.get_subtasks():
655
############# Private functions #################
658
def __focus_out(self,w=None,e=None) : #pylint: disable-msg=W0613
659
#We should only close if the pointer click is out of the calendar !
660
p = self.calendar.window.get_pointer()
661
s = self.calendar.get_size()
662
if not(0 <= p[0] <= s[0] and 0 <= p[1] <= s[1]) :
663
self.__close_calendar()
666
def __close_calendar(self,widget=None,e=None) : #pylint: disable-msg=W0613
668
self.__opened_date = ''
669
gtk.gdk.pointer_ungrab()
670
self.calendar.grab_remove()
672
self.cal_widget.disconnect(self.sigid)
674
if self.sigid_month :
675
self.cal_widget.disconnect(self.sigid_month)
676
self.sigid_month = None