36
36
from webbrowser import open as openurl
38
38
from GTG.gtk.editor import taskviewserial
39
from GTG.tools import urlregex
39
from GTG.tools import urlregex
41
41
separators = [' ', ',', '\n', '\t', '!', '?', ';', '\0', '(', ')']
42
#those separators are only separators if followed by a space. Else, they
42
# those separators are only separators if followed by a space. Else, they
43
# are part of the word
44
44
specials_separators = ['.', '/']
50
50
class TaskView(gtk.TextView):
51
51
__gtype_name__ = 'HyperTextView'
52
52
__gsignals__ = {'anchor-clicked': (gobject.SIGNAL_RUN_LAST,
53
None, (str, str, int))}
53
None, (str, str, int))}
54
54
__gproperties__ = {
55
55
'link': (gobject.TYPE_PYOBJECT, 'link color',
56
'link color of TextView', gobject.PARAM_READWRITE),
56
'link color of TextView', gobject.PARAM_READWRITE),
57
57
'failedlink': (gobject.TYPE_PYOBJECT, 'failed link color',
58
'failed link color of TextView', gobject.PARAM_READWRITE),
58
'failed link color of TextView',
59
gobject.PARAM_READWRITE),
59
60
'active': (gobject.TYPE_PYOBJECT, 'active color',
60
'active color of TextView', gobject.PARAM_READWRITE),
61
'active color of TextView', gobject.PARAM_READWRITE),
61
62
'hover': (gobject.TYPE_PYOBJECT, 'link:hover color',
62
63
'link:hover color of TextView', gobject.PARAM_READWRITE),
63
64
'tag': (gobject.TYPE_PYOBJECT, 'tag color',
64
'tag color of TextView', gobject.PARAM_READWRITE),
65
'tag color of TextView', gobject.PARAM_READWRITE),
65
66
'done': (gobject.TYPE_PYOBJECT, 'link color',
66
'link color of TextView', gobject.PARAM_READWRITE),
67
'link color of TextView', gobject.PARAM_READWRITE),
67
68
'indent': (gobject.TYPE_PYOBJECT, 'indent color',
68
'indent color of TextView', gobject.PARAM_READWRITE),
69
'indent color of TextView', gobject.PARAM_READWRITE),
71
72
def do_get_property(self, prop):
81
82
raise AttributeError('unknown property %s' % prop.name)
83
#Yes, we want to redefine the buffer. Disabling pylint on that error.
84
# Yes, we want to redefine the buffer. Disabling pylint on that error.
84
85
def __init__(self, requester, clipboard, buffer=None):
85
#pylint: disable-msg=W0622
86
# pylint: disable-msg=W0622
86
87
gtk.TextView.__init__(self, buffer)
87
88
self.buff = self.get_buffer()
88
89
self.req = requester
90
91
self.link = {'background': 'white', 'foreground': '#007bff',
91
'underline': pango.UNDERLINE_SINGLE,
92
'strikethrough': False}
92
'underline': pango.UNDERLINE_SINGLE,
93
'strikethrough': False}
93
94
self.failedlink = {'background': 'white', 'foreground': '#ff5454',
94
'underline': pango.UNDERLINE_NONE,
95
'strikethrough': False}
95
'underline': pango.UNDERLINE_NONE,
96
'strikethrough': False}
96
97
self.done = {'background': 'white', 'foreground': 'gray',
97
'strikethrough': True}
98
'strikethrough': True}
98
99
self.active = {'background': 'light gray', 'foreground': '#ff1e00',
99
'underline': pango.UNDERLINE_SINGLE}
100
'underline': pango.UNDERLINE_SINGLE}
100
101
self.hover = {'background': 'light gray'}
101
102
self.tag = {'background': "#FFea00", 'foreground': 'black'}
102
103
self.indent = {'scale': 1.4, 'editable': False, 'left-margin': 10,
103
"accumulative-margin": True}
104
"accumulative-margin": True}
105
106
###### Tag we will use ######
106
107
# We use the tag table (tag are defined here
108
109
self.table = self.buff.get_tag_table()
110
111
self.title_tag = self.buff.create_tag("title", foreground="#007bff",
111
scale=1.6, underline=1)
112
scale=1.6, underline=1)
112
113
self.title_tag.set_property("pixels-above-lines", 10)
113
114
self.title_tag.set_property("pixels-below-lines", 10)
114
115
# Tag for highlight (tags are automatically added to the tag table)
115
116
self.buff.create_tag("fluo", background="#F0F")
116
117
# Tag for bullets
117
118
self.buff.create_tag("bullet", scale=1.6)
118
#end = self.buff.get_end_iter()
119
# end = self.buff.get_end_iter()
120
#This is the list of all the links in our task
121
# This is the list of all the links in our task
122
#This is a simple stack used by the serialization
123
# This is a simple stack used by the serialization
123
124
self.__tag_stack = {}
126
127
self.connect('motion-notify-event', self._motion)
127
128
self.connect('focus-out-event',
128
lambda w, e: self.table.foreach(self.__tag_reset, e.window))
129
lambda w, e: self.table.foreach(self.__tag_reset,
129
131
self.insert_sigid = self.buff.connect('insert-text',
130
132
self._insert_at_cursor)
131
133
self.delete_sigid = self.buff.connect("delete-range",
133
135
self.connect('copy-clipboard', self.copy_clipboard, "copy")
134
136
self.connect('cut-clipboard', self.copy_clipboard, "cut")
135
137
self.connect('paste-clipboard', self.paste_clipboard)
137
139
self.connect('drag-data-received', self.drag_receive)
139
#All the typical properties of our textview
141
# All the typical properties of our textview
140
142
self.set_wrap_mode(gtk.WRAP_WORD)
141
143
self.set_editable(True)
142
144
self.set_cursor_visible(True)
143
145
self.buff.set_modified(False)
145
#Let's try with serializing
147
# Let's try with serializing
146
148
self.mime_type = 'application/x-gtg-task'
147
149
serializer = taskviewserial.Serializer()
148
150
unserializer = taskviewserial.Unserializer(self)
149
151
self.buff.register_serialize_format(self.mime_type,
150
serializer.serialize, None)
152
serializer.serialize, None)
151
153
self.buff.register_deserialize_format(self.mime_type,
152
unserializer.unserialize, None)
154
unserializer.unserialize, None)
154
#The list of callbacks we have to set
156
# The list of callbacks we have to set
155
157
self.remove_tag_callback = None
156
158
self.add_tag_callback = None
157
159
self.get_tagslist = None
158
160
self.get_subtasks = None
159
self.remove_subtask =None
161
self.remove_subtask = None
160
162
self.__refresh_cb = None # refresh the editor window
161
self.open_task = None # open another task
162
self.new_subtask_callback = None # create a subtask
163
self.save_task = None #This will save the task without refreshing all
163
self.open_task = None # open another task
164
self.new_subtask_callback = None # create a subtask
165
self.save_task = None # This will save the task without refreshing all
165
#The signal emitted each time the buffer is modified
166
#Putting it at the end to avoid doing it too much when starting
167
# The signal emitted each time the buffer is modified
168
# Putting it at the end to avoid doing it too much when starting
167
169
self.modified_sigid = self.buff.connect("changed", self.modified)
168
170
self.backspace_sigid = self.connect("backspace", self.backspace)
169
171
self.tobe_refreshed = False
183
185
self.modified(full=True)
184
186
self.stop_emission('drag-data-received')
187
#editable means that the user can edit the taskview
188
#this is initially set at False and then to True once the editor window
190
#this is used to avoid saving the task when the window is still
188
# editable means that the user can edit the taskview
189
# this is initially set at False and then to True once the editor window
191
# this is used to avoid saving the task when the window is still
192
193
def set_editable(self, boule):
193
194
self.editable = boule
195
196
def get_editable(self):
196
197
return self.editable
198
#This function is called to refresh the editor
199
#Specially when we change the title
199
# This function is called to refresh the editor
200
# Specially when we change the title
200
201
def refresh(self, title):
201
202
if self.__refresh_cb:
202
203
self.__refresh_cb(title)
204
205
def refresh_callback(self, funct):
205
206
self.__refresh_cb = funct
207
#This callback is called to add a new tag
208
# This callback is called to add a new tag
208
209
def set_add_tag_callback(self, funct):
209
210
self.add_tag_callback = funct
211
#This callback is called to add a new tag
212
# This callback is called to add a new tag
212
213
def set_remove_tag_callback(self, funct):
213
214
self.remove_tag_callback = funct
215
#This callback is called to have the list of tags of a task
216
# This callback is called to have the list of tags of a task
216
217
def set_get_tagslist_callback(self, funct):
217
218
self.get_tagslist = funct
219
#This callback is called to create a new subtask
220
# This callback is called to create a new subtask
220
221
def set_subtask_callback(self, funct):
221
222
self.new_subtask_callback = funct
223
#This callback is called to open another task
224
# This callback is called to open another task
224
225
def open_task_callback(self, funct):
225
226
self.open_task = funct
227
#This was historically a callback but it returns the title
228
# This was historically a callback but it returns the title
228
229
def get_subtasktitle(self, tid):
229
230
task = self.req.get_task(tid)
235
#This callback is called to get the list of tid of subtasks
236
# This callback is called to get the list of tid of subtasks
236
237
def subtasks_callback(self, funct):
237
238
self.get_subtasks = funct
239
#This callback is called to remove a subtask by its pid
240
# This callback is called to remove a subtask by its pid
240
241
def removesubtask_callback(self, funct):
241
242
self.remove_subtask = funct
243
244
def save_task_callback(self, funct):
244
245
self.save_task = funct
246
#Buffer related functions
247
#Those functions are higly related and should always be symetrical
248
#See also the serializing functions
247
# Buffer related functions
248
# Those functions are higly related and should always be symetrical
249
# See also the serializing functions
249
250
#### The "Set text" group ########
250
#This set the text of the buffer (and replace any existing one)
251
#without deserializing (used for the title)
251
# This set the text of the buffer (and replace any existing one)
252
# without deserializing (used for the title)
252
253
def set_text(self, stri):
253
254
self.buff.set_text(stri)
254
#This append text at the end of the buffer after deserializing it
255
# This append text at the end of the buffer after deserializing it
255
257
def insert(self, text, _iter=None):
256
258
if _iter is None:
257
259
_iter = self.buff.get_end_iter()
258
#Ok, this line require an integer at some place !
259
#disable the insert and modified signals
260
# Ok, this line require an integer at some place !
261
# disable the insert and modified signals
260
262
reconnect_insert = False
261
263
reconnect_modified = False
262
264
if self.insert_sigid:
268
270
self.modified_sigid = False
269
271
reconnect_modified = True
272
274
self.buff.deserialize(self.buff, self.mime_type, _iter, text)
275
277
if reconnect_insert:
276
278
self.insert_sigid = self.buff.connect('insert-text',
277
self._insert_at_cursor)
279
self._insert_at_cursor)
278
280
if reconnect_modified:
279
281
self.modified_sigid = self.buff.connect("changed", self.modified)
281
#This insert raw text without deserializing
283
# This insert raw text without deserializing
282
284
def insert_text(self, text, _iter=None):
283
285
if _iter is None:
284
286
_iter = self.buff.get_end_iter()
285
287
self.buff.insert(_iter, text)
287
#We cannot get an insert in the title
289
# We cannot get an insert in the title
288
290
def get_insert(self):
289
291
mark = self.buff.get_insert()
290
292
itera = self.buff.get_iter_at_mark(mark)
315
317
def create_anchor_tag(self, b, anchor, text=None, typ=None):
316
#We cannot have two tags with the same name
317
#That's why the link tag has no name
318
#but it has a "is_anchor" property
318
# We cannot have two tags with the same name
319
# That's why the link tag has no name
320
# but it has a "is_anchor" property
319
321
if typ == "http":
320
322
linktype = 'link'
321
#By default, the type is a subtask
323
# By default, the type is a subtask
323
325
task = self.req.get_task(anchor)
324
326
if task and task.get_status() == "Active":
353
355
if ss.begins_tag(t) and ee.ends_tag(t):
356
#pylint: disable-msg=W0142
358
# pylint: disable-msg=W0142
357
359
texttag = buff.create_tag(None, **self.get_property('tag'))
358
360
texttag.set_data('is_tag', True)
359
361
texttag.set_data('tagname', tag)
360
#This one is for marks
362
# This one is for marks
362
364
self.__apply_tag_to_mark(s, e, tag=texttag)
364
#Apply the tag tag to a set of TextMarks (not Iter)
365
#Also change the subtask title if needed
366
# Apply the tag tag to a set of TextMarks (not Iter)
367
# Also change the subtask title if needed
366
368
def apply_subtask_tag(self, buff, subtask, s, e):
367
369
i_s = buff.get_iter_at_mark(s)
368
370
i_e = buff.get_iter_at_mark(e)
369
371
tex = buff.get_text(i_s, i_e)
370
#we don't accept \n in a subtask title
372
# we don't accept \n in a subtask title
373
375
while i_e.get_char() != "\n":
388
390
buff.delete_mark(e)
390
392
def create_indent_tag(self, buff, level):
391
#pylint: disable-msg=W0142
393
# pylint: disable-msg=W0142
392
394
tag = buff.create_tag(None, **self.get_property('indent'))
393
395
tag.set_data('is_indent', True)
394
396
tag.set_data('indent_level', level)
397
#Insert a list of subtasks at the end of the buffer
399
# Insert a list of subtasks at the end of the buffer
398
400
def insert_subtasks(self, st_list):
399
401
for tid in st_list:
400
402
line_nbr = self.buff.get_end_iter().get_line()
401
#Warning, we have to take the next line !
402
self.write_subtask(self.buff, line_nbr+1, tid)
403
# Warning, we have to take the next line !
404
self.write_subtask(self.buff, line_nbr + 1, tid)
404
#Insert a list of tag in the first line of the buffer
406
# Insert a list of tag in the first line of the buffer
405
407
def insert_tags(self, tag_list):
406
#First, we don't insert tags that are already present
408
# First, we don't insert tags that are already present
407
409
for t in self.get_tagslist():
408
410
if t in tag_list:
409
411
tag_list.remove(t)
410
412
if len(tag_list) > 0:
411
#We insert them just after the title
412
#We use the current first line if it begins with a tag
413
# We insert them just after the title
414
# We use the current first line if it begins with a tag
413
415
firstline = self.buff.get_iter_at_line(1)
415
417
for tt in firstline.get_tags():
430
432
# if not text.strip(", "):
431
433
# newline = False
432
434
# firstline.forward_to_line_end()
435
437
firstline = self.buff.get_iter_at_line(0)
436
438
firstline.forward_to_line_end()
437
439
self.insert_text("\n", firstline)
438
440
firstline = self.buff.get_iter_at_line(1)
439
441
line_mark = self.buff.create_mark("firstline", firstline, False)
440
#self.tv.insert_at_mark(buf, line_mark, "\n")
442
# self.tv.insert_at_mark(buf, line_mark, "\n")
441
443
ntags = len(tag_list)
442
444
for t in tag_list:
443
445
ntags = ntags - 1
475
477
conti = start.forward_line()
477
479
conti = start.forward_to_line_end()
478
#we go to the next line, just after the title
480
# we go to the next line, just after the title
479
481
start.forward_line()
480
482
end = self.buff.get_end_iter()
481
483
texte = self.buff.serialize(self.buff, self.mime_type, start, end)
484
#Get the title of the task (aka the first line of the buffer)
486
# Get the title of the task (aka the first line of the buffer)
485
488
def get_title(self):
486
489
start = self.buff.get_start_iter()
487
490
end = self.buff.get_start_iter()
488
491
end.forward_to_line_end()
489
#The boolean stays True as long as we are in the buffer
492
# The boolean stays True as long as we are in the buffer
491
494
while conti and not end.ends_tag(self.table.lookup("title")):
492
495
conti = end.forward_line()
494
497
conti = end.forward_to_line_end()
495
#We don't want to deserialize the title
496
#Let's get the pure text directly
498
# We don't want to deserialize the title
499
# Let's get the pure text directly
497
500
title = unicode(self.buff.get_text(start, end))
498
#Let's strip blank lines
501
# Let's strip blank lines
499
502
stripped = title.strip(' \n\t')
502
505
### PRIVATE FUNCTIONS #####################################################
503
#This function is called so frequently that we should optimize it more.
506
# This function is called so frequently that we should optimize it more.
504
507
def modified(self, buff=None, full=False, refresheditor=True):
505
508
"""Called when the buffer has been modified.
515
518
cursor_mark = buff.get_insert()
516
519
cursor_iter = buff.get_iter_at_mark(cursor_mark)
517
520
table = buff.get_tag_table()
518
#This should be called only if we are on the title line
520
#But we should still get the title_end iter
521
# This should be called only if we are on the title line
523
# But we should still get the title_end iter
521
524
if full or self.is_at_title(buff, cursor_iter):
522
#The apply title is very expensive because
523
#It involves refreshing the whole task tree
525
# The apply title is very expensive because
526
# It involves refreshing the whole task tree
524
527
title_end = self._apply_title(buff, refresheditor)
527
530
local_start = title_end.copy()
528
531
local_end = buff.get_end_iter()
530
#We analyse only the current line
533
# We analyse only the current line
531
534
local_start = cursor_iter.copy()
532
535
local_start.set_line(local_start.get_line())
533
536
local_end = cursor_iter.copy()
534
537
local_end.forward_lines(2)
535
#if full=False we detect tag only on the current line
538
# if full=False we detect tag only on the current line
537
#The following 3 lines are a quick ugly fix for bug #359469
540
# The following 3 lines are a quick ugly fix for bug #359469
538
541
# temp = buff.get_iter_at_line(1)
539
542
# temp.backward_char()
540
543
# self._detect_tag(buff, temp, buff.get_end_iter())
541
#This should be the good line
544
# This should be the good line
542
545
self._detect_tag(buff, local_start, local_end)
543
546
self._detect_url(buff, local_start, local_end)
545
#subt_list = self.get_subtasks()
546
#First, we remove the olds tags
548
# subt_list = self.get_subtasks()
549
# First, we remove the olds tags
549
def subfunc(texttag, data=None): #pylint: disable-msg=W0613
552
def subfunc(texttag, data=None): # pylint: disable-msg=W0613
550
553
if texttag.get_data('is_subtask'):
551
554
tag_list.append(texttag)
556
559
buff.remove_tag(t, start, end)
559
#We apply the hyperlink tag to subtask
562
# We apply the hyperlink tag to subtask
560
563
for s in self.get_subtasks():
561
564
start_mark = buff.get_mark(s)
562
565
# "applying %s to %s - %s"%(s, start_mark, end_mark)
564
#In fact, the subtask mark always go to the end of line.
567
# In fact, the subtask mark always go to the end of line.
565
568
start_i = buff.get_iter_at_mark(start_mark)
566
569
if self._get_indent_level(start_i) > 0:
567
570
start_i.forward_to_line_end()
568
end_mark = buff.create_mark("/%s"%s, start_i, False)
571
end_mark = buff.create_mark("/%s" % s, start_i, False)
569
572
self.apply_subtask_tag(buff, s, start_mark, end_mark)
571
574
self.remove_subtask(s)
574
#Now we apply the tag tag to the marks
576
# Now we apply the tag tag to the marks
575
577
for t in self.get_tagslist():
576
578
start_mark = buff.get_mark(t)
577
end_mark = buff.get_mark("/%s"%t)
579
end_mark = buff.get_mark("/%s" % t)
578
580
# "applying %s to %s - %s"%(t, start_mark, end_mark)
579
581
if start_mark and end_mark:
580
582
self.apply_tag_tag(buff, t, start_mark, end_mark)
582
#Ok, we took care of the modification
584
# Ok, we took care of the modification
583
585
self.buff.set_modified(False)
584
#Else we save the task anyway (but without refreshing all)
586
# Else we save the task anyway (but without refreshing all)
585
587
if self.save_task:
588
#Detect URL in the tasks
590
# Detect URL in the tasks
590
592
def _detect_url(self, buff, start, end):
591
#subt_list = self.get_subtasks()
592
#First, we remove the olds tags
593
# subt_list = self.get_subtasks()
594
# First, we remove the olds tags
594
596
table = buff.get_tag_table()
658
660
table = buff.get_tag_table()
661
#We must be strictly < than the end_offset. If not, we might
662
#find the beginning of a tag on the nextline
663
# We must be strictly < than the end_offset. If not, we might
664
# find the beginning of a tag on the nextline
663
665
while (it.get_offset() < end.get_offset()) and (it.get_char() != '\0'):
664
666
if it.begins_tag():
665
667
tags = it.get_toggled_tags(True)
667
#removing deleted tags
669
# removing deleted tags
668
670
if ta.get_data('is_tag'):
669
671
tagname = ta.get_data('tagname')
670
672
old_tags.append(tagname)
671
673
buff.remove_tag(ta, start, end)
673
#Removing the marks if they exist
675
# Removing the marks if they exist
674
676
mark1 = buff.get_mark(tagname)
676
678
offset1 = buff.get_iter_at_mark(mark1).get_offset()
677
679
if start.get_offset() <= offset1 <= \
679
681
buff.delete_mark_by_name(tagname)
680
mark2 = buff.get_mark("/%s"%tagname)
682
mark2 = buff.get_mark("/%s" % tagname)
682
684
offset2 = buff.get_iter_at_mark(mark2).get_offset()
683
685
if start.get_offset() <= offset2 <= \
685
buff.delete_mark_by_name("/%s"%tagname)
687
buff.delete_mark_by_name("/%s" % tagname)
686
688
it.forward_char()
688
690
# Set iterators for word
720
722
if (word_end.compare(word_start) > 0):
721
723
my_word = buff.get_text(word_start, word_end)
722
724
# We do something about it
723
#We want a tag bigger than the simple "@"
724
#and it shouldn't start with @@ (bug 531553)
725
# We want a tag bigger than the simple "@"
726
# and it shouldn't start with @@ (bug 531553)
725
727
if len(my_word) > 1 and my_word[0] == '@' \
726
and not my_word[1] == '@':
727
#self.apply_tag_tag(buff, my_word, word_start,
728
and not my_word[1] == '@':
729
# self.apply_tag_tag(buff, my_word, word_start,
729
#We will add mark where tag should be applied
731
# We will add mark where tag should be applied
730
732
buff.create_mark(my_word, word_start, True)
731
buff.create_mark("/%s"%my_word, word_end, False)
732
#adding tag to a local list
733
buff.create_mark("/%s" % my_word, word_end, False)
734
# adding tag to a local list
733
735
new_tags.append(my_word)
734
#adding tag to the model
736
# adding tag to the model
735
737
self.add_tag_callback(my_word)
737
739
# We set new word boundaries
758
760
if itera.get_line() == 0:
760
#We are at a line with the title tag applied
762
# We are at a line with the title tag applied
761
763
elif self.title_tag in itera.get_tags():
763
#else, we look if there's something between us and buffer start
765
# else, we look if there's something between us and buffer start
764
766
elif not buff.get_text(buff.get_start_iter(), itera).strip('\n\t '):
769
#When the user removes a selection, we remove subtasks and @tags
771
# When the user removes a selection, we remove subtasks and @tags
772
# from this selection
771
773
def _delete_range(self, buff, start, end):
772
774
# #If we are at the beginning of a mark, put this mark at the end
773
775
# marks = start.get_marks()
774
776
# for m in marks:
775
777
# print m.get_name()
776
778
# buff.move_mark(m, end)
777
#If the begining of the selection is in the middle of an indent
778
#We want to start at the begining
779
tags = start.get_tags()+start.get_toggled_tags(False)
779
# If the begining of the selection is in the middle of an indent
780
# We want to start at the begining
781
tags = start.get_tags() + start.get_toggled_tags(False)
781
783
if (ta.get_data('is_indent')):
782
784
line = start.get_line()
786
788
# endindent = start.copy()
787
789
# endindent.forward_to_tag_toggle(ta)
788
790
# buff.remove_tag(ta, start, endindent)
789
#Now we delete all, char after char
791
# Now we delete all, char after char
790
792
it = start.copy()
791
793
while it.get_offset() <= end.get_offset() and it.get_char() != '\0':
792
794
if it.begins_tag():
793
795
tags = it.get_tags()
795
#removing deleted subtasks
797
# removing deleted subtasks
796
798
if ta.get_data('is_subtask') and it.begins_tag(ta):
797
799
target = ta.get_data('child')
798
#print "removing task %s" %target
800
# print "removing task %s" %target
799
801
self.remove_subtask(target)
800
#removing deleted tags
802
# removing deleted tags
801
803
if ta.get_data('is_tag') and it.begins_tag(ta):
802
804
tagname = ta.get_data('tagname')
803
805
self.remove_tag_callback(tagname)
804
806
if buff.get_mark(tagname):
805
807
buff.delete_mark_by_name(tagname)
806
if buff.get_mark("/%s"%tagname):
807
buff.delete_mark_by_name("/%s"%tagname)
808
if buff.get_mark("/%s" % tagname):
809
buff.delete_mark_by_name("/%s" % tagname)
808
810
if ta.get_data('is_indent'):
809
#Because the indent tag is read only
811
# Because the indent tag is read only
811
813
endtag = it.copy()
812
814
endtag.forward_to_tag_toggle(ta)
813
815
buff.remove_tag(ta, it, endtag)
814
#Also, we want to delete the indent completely,
815
#Even if the selection was in the middle of an indent
816
# Also, we want to delete the indent completely,
817
# Even if the selection was in the middle of an indent
817
819
it.forward_char()
818
#now we really delete the selected stuffs
820
# now we really delete the selected stuffs
819
821
selec = self.buff.get_selection_bounds()
821
823
# print "deleted text is ##%s##" %self.buff.get_text(selec[0],
851
853
title_start = start.copy()
852
854
if linecount > line_nbr:
853
855
# Applying title on the first line
854
title_end = buff.get_iter_at_line(line_nbr-1)
856
title_end = buff.get_iter_at_line(line_nbr - 1)
855
857
title_end.forward_to_line_end()
856
858
stripped = buff.get_text(title_start, title_end).strip('\n\t ')
857
859
# Here we ignore lines that are blank
858
860
# Title is the first written line
859
861
while line_nbr <= linecount and not stripped:
861
title_end = buff.get_iter_at_line(line_nbr-1)
863
title_end = buff.get_iter_at_line(line_nbr - 1)
862
864
title_end.forward_to_line_end()
863
865
stripped = buff.get_text(title_start, title_end).strip('\n\t ')
864
866
# Or to all the buffer if there is only one line
876
878
end_i = self.write_subtask(buff, line_nbr, anchor, level=level)
879
#Write the subtask then return the iterator at the end of the line
881
# Write the subtask then return the iterator at the end of the line
880
882
def write_subtask(self, buff, line_nbr, anchor, level=1):
881
#disable the insert signal to avoid recursion
882
#firstly, we check that the subtask exists !
883
# disable the insert signal to avoid recursion
884
# firstly, we check that the subtask exists !
883
885
if not self.req.has_task(anchor):
885
887
reconnect_insert = False
893
895
self.modified_sigid = False
894
896
reconnect_modified = True
896
#First, we insert the request \n
897
#If we don't do this, the content of next line will automatically
898
#be in the subtask title
898
# First, we insert the request \n
899
# If we don't do this, the content of next line will automatically
900
# be in the subtask title
899
901
start_i = buff.get_iter_at_line(line_nbr)
900
902
start_i.forward_to_line_end()
901
903
buff.insert(start_i, "\n")
902
#Ok, now we can start working
904
# Ok, now we can start working
903
905
start_i = buff.get_iter_at_line(line_nbr)
904
906
end_i = start_i.copy()
905
#We go back at the end of the previous line
907
# We go back at the end of the previous line
906
908
# start_i.backward_char()
907
909
# #But only if this is not the title.
908
910
insert_enter = False
918
920
newline = self.get_subtasktitle(anchor)
919
921
end_i = buff.get_iter_at_mark(end)
920
922
startm = buff.create_mark(anchor, end_i, True)
921
#Putting the subtask marks around the title
923
# Putting the subtask marks around the title
922
924
self.insert_at_mark(buff, end, newline)
923
925
end_i = buff.get_iter_at_mark(end)
924
endm = buff.create_mark("/%s"%anchor, end_i, False)
925
#put the tag on the marks
926
endm = buff.create_mark("/%s" % anchor, end_i, False)
927
# put the tag on the marks
926
928
self.apply_subtask_tag(buff, anchor, startm, endm)
927
#buff.delete_mark(start)
928
#buff.delete_mark(end)
929
# buff.delete_mark(start)
930
# buff.delete_mark(end)
930
932
if reconnect_insert:
931
933
self.insert_sigid = self.buff.connect('insert-text',
932
self._insert_at_cursor)
934
self._insert_at_cursor)
933
935
if reconnect_modified:
934
936
self.modified_sigid = self.buff.connect("changed", self.modified)
981
983
self.buff.place_cursor(end)
983
985
def insert_indent(self, buff, start_i, level, enter=True):
984
#We will close the current subtask tag
986
# We will close the current subtask tag
985
987
list_stag = start_i.get_toggled_tags(False)
987
989
for t in list_stag:
988
990
if t.get_data('is_subtask'):
990
#maybe the tag was not toggled off here but we were in the middle
992
# maybe the tag was not toggled off here but we were in the middle
992
994
list_stag = start_i.get_tags()
993
995
for t in list_stag:
994
996
if t.get_data('is_subtask'):
997
#We will remove the tag from the whole text
999
# We will remove the tag from the whole text
998
1000
subtid = stag.get_data('child')
999
#We move the end_subtask mark to here
1000
#We have to create a temporary mark with left gravity
1001
#It will be later replaced by the good one with right gravity
1001
# We move the end_subtask mark to here
1002
# We have to create a temporary mark with left gravity
1003
# It will be later replaced by the good one with right gravity
1002
1004
temp_mark = self.buff.create_mark("temp", start_i, True)
1004
1006
end = buff.create_mark("end", start_i, False)
1006
1008
buff.insert(start_i, "\n")
1008
#Moving the end of subtask mark to the position of the temp mark
1010
# Moving the end of subtask mark to the position of the temp mark
1010
1012
itera = buff.get_iter_at_mark(temp_mark)
1011
buff.move_mark_by_name("/%s"%subtid, itera)
1013
buff.move_mark_by_name("/%s" % subtid, itera)
1012
1014
buff.delete_mark(temp_mark)
1013
#The mark has right gravity but because we put it on the left
1014
#of the newly inserted \n, it will not move anymore.
1015
# The mark has right gravity but because we put it on the left
1016
# of the newly inserted \n, it will not move anymore.
1016
1018
itera = buff.get_iter_at_mark(end)
1017
#We should never have an indentation at 0.
1018
#This is normally not needed and purely defensive
1019
# We should never have an indentation at 0.
1020
# This is normally not needed and purely defensive
1019
1021
if itera.get_line() <= 0:
1020
1022
itera = buff.get_iter_at_line(1)
1021
1023
start = buff.create_mark("start", itera, True)
1022
1024
indentation = ""
1023
#adding two spaces by level
1025
# adding two spaces by level
1025
indentation = indentation + (level-1)*spaces
1027
indentation = indentation + (level - 1) * spaces
1028
indentation = "%s%s "%(indentation, self.bullet1)
1030
indentation = "%s%s " % (indentation, self.bullet1)
1029
1031
buff.insert(itera, indentation)
1030
1032
indenttag = self.create_indent_tag(buff, level)
1031
1033
self.__apply_tag_to_mark(start, end, tag=indenttag)
1061
1063
current_indent = ta.get_data('indent_level')
1062
1064
return current_indent
1064
#Method called on copy and cut actions
1065
#param is either "cut" or "copy"
1066
# Method called on copy and cut actions
1067
# param is either "cut" or "copy"
1066
1068
def copy_clipboard(self, widget, param=None):
1067
1069
clip = gtk.clipboard_get(gdk.SELECTION_CLIPBOARD)
1069
#First, we analyse the selection to put in our own
1070
#GTG clipboard a selection with description of subtasks
1071
# First, we analyse the selection to put in our own
1072
# GTG clipboard a selection with description of subtasks
1071
1073
bounds = self.buff.get_selection_bounds()
1085
1087
self.stop_emission("copy_clipboard")
1088
1090
def paste_clipboard(self, widget, param=None):
1089
1091
clip = gtk.clipboard_get(gdk.SELECTION_CLIPBOARD)
1090
#if the clipboard text is the same are our own internal
1091
#clipboard text, it means that we can paste from our own clipboard
1092
#else, that we can empty it.
1092
# if the clipboard text is the same are our own internal
1093
# clipboard text, it means that we can paste from our own clipboard
1094
# else, that we can empty it.
1093
1095
our_paste = self.clipboard.paste_text()
1094
if our_paste != None and clip.wait_for_text() == our_paste:
1095
#first, we delete the current selection
1096
if our_paste is not None and clip.wait_for_text() == our_paste:
1097
# first, we delete the current selection
1096
1098
self.buff.delete_selection(False, True)
1097
1099
for line in self.clipboard.paste():
1098
1100
if line[0] == 'text':
1099
1101
self.buff.insert_at_cursor(line[1])
1100
1102
if line[0] == 'subtask':
1102
1104
self.new_subtask_callback(tid=tid)
1103
1105
mark = self.buff.get_insert()
1104
1106
line_nbr = self.buff.get_iter_at_mark(mark).get_line()
1105
#we must paste the \n before inserting the subtask
1106
#else, we will start another subtask
1107
# we must paste the \n before inserting the subtask
1108
# else, we will start another subtask
1107
1109
self.buff.insert_at_cursor("\n")
1108
1110
self.write_subtask(self.buff, line_nbr, tid)
1110
#we handle ourselves the pasting
1112
# we handle ourselves the pasting
1111
1113
self.stop_emission("paste_clipboard")
1114
#we keep the normal pasting by not interupting the signal
1116
# we keep the normal pasting by not interupting the signal
1115
1117
self.clipboard.clear()
1117
#Function called each time the user inputs a letter
1119
# Function called each time the user inputs a letter
1118
1120
def _insert_at_cursor(self, tv, itera, tex, leng):
1119
#We don't paste the bullet
1121
# We don't paste the bullet
1120
1122
if tex.strip() != self.bullet1:
1121
#print "text ###%s### inserted length = %s" %(tex, leng)
1122
#disable the insert signal to avoid recursion
1123
# print "text ###%s### inserted length = %s" %(tex, leng)
1124
# disable the insert signal to avoid recursion
1123
1125
self.buff.disconnect(self.insert_sigid)
1124
1126
self.insert_sigid = False
1125
1127
self.buff.disconnect(self.modified_sigid)
1126
1128
self.modified_sigid = False
1128
#First, we will get the actual indentation value
1129
#The nbr just before the \n
1130
# First, we will get the actual indentation value
1131
# The nbr just before the \n
1130
1132
line_nbr = itera.get_line()
1131
1133
start_line = itera.copy()
1132
1134
start_line.set_line(line_nbr)
1138
1140
for ta in tags:
1139
1141
if ta.get_data('is_subtask'):
1140
1142
subtask_nbr = ta.get_data('child')
1141
#Maybe we are simply at the end of the tag
1143
# Maybe we are simply at the end of the tag
1142
1144
if not subtask_nbr and itera.ends_tag():
1143
1145
for ta in itera.get_toggled_tags(False):
1144
1146
if ta.get_data('is_subtask'):
1145
1147
subtask_nbr = ta.get_data('child')
1147
#New line: the user pressed enter !
1148
#If the line begins with "-", it's a new subtask !
1149
# New line: the user pressed enter !
1150
# If the line begins with "-", it's a new subtask !
1149
1151
if tex == '\n':
1150
1152
self.buff.create_mark("insert_point", itera, True)
1151
#First, we close tag tags.
1152
#If we are at the end of a tag, we look for closed tags
1153
# First, we close tag tags.
1154
# If we are at the end of a tag, we look for closed tags
1153
1155
closed_tag = None
1154
1156
cutting_subtask = False
1155
1157
if itera.ends_tag():
1156
1158
list_stag = itera.get_toggled_tags(False)
1157
#Or maybe we are in the middle of a tag
1159
# Or maybe we are in the middle of a tag
1159
1161
list_stag = itera.get_tags()
1160
1162
for t in list_stag:
1175
1177
restofline = end_line.get_slice(realend)
1176
1178
restofline.strip()
1178
#If indent is 0, We check if we created a new task
1179
#the "-" might be after a space
1180
#Python 2.5 should allow both tests in one
1180
# If indent is 0, We check if we created a new task
1181
# the "-" might be after a space
1182
# Python 2.5 should allow both tests in one
1181
1183
if current_indent == 0:
1182
1184
if (line.startswith('-') or line.startswith(' -')) \
1183
and line.lstrip(' -').strip() != "":
1185
and line.lstrip(' -').strip() != "":
1184
1186
line = line.lstrip(' -')
1185
1187
end_i = self.__newsubtask(self.buff, line,
1187
#Here, we should increment indent level
1188
#If we inserted enter in the middle of a line
1189
# Here, we should increment indent level
1190
# If we inserted enter in the middle of a line
1189
1191
if restofline and restofline.strip() != "":
1190
#it means we have two subtask to create
1191
if self.buff.get_line_count() > line_nbr+1:
1192
#but don't merge with the next line
1192
# it means we have two subtask to create
1193
if self.buff.get_line_count() > line_nbr + 1:
1194
# but don't merge with the next line
1193
1195
itera = self.buff.get_iter_at_line(
1195
1197
self.buff.insert(itera, "\n\n")
1196
1198
self.__newsubtask(self.buff, restofline,
1199
1201
self.insert_indent(self.buff, end_i, 1,
1201
1203
tv.emit_stop_by_name('insert-text')
1203
1205
self.buff.insert(itera, "\n")
1204
1206
tv.emit_stop_by_name('insert-text')
1206
#Then, if indent > 0, we increment it
1207
#First step: we preserve it.
1208
# Then, if indent > 0, we increment it
1209
# First step: we preserve it.
1209
if not line.lstrip("%s "%self.bullet1):
1210
#if we didn't write a task, we remove the indent
1211
#we check if the iterator is well at the end of
1211
if not line.lstrip("%s " % self.bullet1):
1212
# if we didn't write a task, we remove the indent
1213
# we check if the iterator is well at the end of
1213
1215
if end_line.ends_line():
1214
1216
self.deindent(itera, newlevel=0)
1215
#else, it means that we pressed enter before
1217
# else, it means that we pressed enter before
1218
#we first put the subtask one line below
1220
# we first put the subtask one line below
1219
1221
itera2 = self.buff.get_iter_at_line(line_nbr)
1220
1222
self.buff.insert(itera2, "\n")
1221
#and increment the new white line
1223
# and increment the new white line
1222
1224
itera2 = self.buff.get_iter_at_line(line_nbr)
1223
1225
self.insert_indent(self.buff, itera2,
1224
current_indent, enter=False)
1226
current_indent, enter=False)
1225
1227
elif current_indent == 1:
1226
1228
self.insert_indent(self.buff, itera,
1228
#we stop the signal in all cases
1230
# we stop the signal in all cases
1229
1231
tv.emit_stop_by_name('insert-text')
1230
#Then we close the tag tag
1232
# Then we close the tag tag
1232
1234
insert_mark = self.buff.get_mark("insert_point")
1233
1235
insert_iter = self.buff.get_iter_at_mark(insert_mark)
1234
1236
self.buff.move_mark_by_name("/%s" % closed_tag,
1236
1238
self.buff.delete_mark(insert_mark)
1237
1239
if cutting_subtask:
1238
1240
cursor = self.buff.get_iter_at_mark(
1243
1245
text = self.buff.get_text(cursor, endl)
1244
1246
anchor = self.new_subtask_callback(text)
1245
1247
self.buff.create_mark(anchor, cursor, True)
1246
self.buff.create_mark("/%s"%anchor, endl, False)
1248
self.buff.create_mark("/%s" % anchor, endl, False)
1247
1249
self.modified(full=True)
1248
#The user entered something else than \n
1250
# The user entered something else than \n
1250
#We are on an indented line without subtask ? Create it !
1252
# We are on an indented line without subtask ? Create it !
1251
1253
if current_indent > 0 and not subtask_nbr:
1252
1254
if itera.starts_line():
1253
#we are at the start of an existing subtask
1254
#we simply move that subtask down
1255
# we are at the start of an existing subtask
1256
# we simply move that subtask down
1255
1257
self.buff.insert(itera, "\n")
1256
1258
itera2 = self.buff.get_iter_at_line(line_nbr)
1257
1259
self.buff.insert(itera2, tex)
1260
1262
self.buff.place_cursor(itera3)
1261
1263
tv.emit_stop_by_name('insert-text')
1263
#self.__newsubtask(self.buff, tex, line_nbr,
1265
# self.__newsubtask(self.buff, tex, line_nbr,
1264
1266
# level=current_indent)
1265
1267
anchor = self.new_subtask_callback(tex)
1266
1268
self.buff.create_mark(anchor, itera, True)
1267
self.buff.create_mark("/%s"%anchor, itera, False)
1269
self.buff.create_mark("/%s" % anchor, itera, False)
1268
1270
self.insert_sigid = self.buff.connect('insert-text',
1269
self._insert_at_cursor)
1271
self._insert_at_cursor)
1270
1272
self.connect('key_press_event', self._keypress)
1271
1273
self.modified_sigid = self.buff.connect("changed", self.modified)
1273
1275
def _keypress(self, widget, event):
1274
1276
# Check for Ctrl-Return/Enter
1275
1277
if event.state & gtk.gdk.CONTROL_MASK and \
1276
event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter):
1278
event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter):
1277
1279
buff = self.buff
1278
1280
cursor_mark = buff.get_insert()
1279
1281
cursor_iter = buff.get_iter_at_mark(cursor_mark)
1303
1305
if newlevel > 0:
1306
1308
if newlevel < 0:
1307
1309
print "bug: no is_indent tag on that line"
1308
#startline.backward_char()
1309
#We make a temp mark where we should insert the new indent
1310
#tempm = self.buff.create_mark("temp", startline)
1310
# startline.backward_char()
1311
# We make a temp mark where we should insert the new indent
1312
# tempm = self.buff.create_mark("temp", startline)
1311
1313
self.buff.disconnect(self.delete_sigid)
1312
#print "deintdent-delete: %s" %self.buff.get_text(startline, itera)
1314
# print "deintdent-delete: %s" %self.buff.get_text(startline, itera)
1313
1315
self.buff.delete(startline, itera)
1314
#For the day when we will have different indent levels
1315
#newiter = self.buff.get_iter_at_mark(tempm)
1316
#self.buff.delete_mark(tempm)
1317
#self.insert_indent(self.buff, newiter, newlevel, enter=False)
1316
# For the day when we will have different indent levels
1317
# newiter = self.buff.get_iter_at_mark(tempm)
1318
# self.buff.delete_mark(tempm)
1319
# self.insert_indent(self.buff, newiter, newlevel, enter=False)
1318
1320
self.delete_sigid = self.buff.connect("delete-range",
1321
1323
def backspace(self, tv):
1322
1324
self.buff.disconnect(self.insert_sigid)
1323
1325
insert_mark = self.buff.get_insert()
1324
1326
insert_iter = self.buff.get_iter_at_mark(insert_mark)
1325
#All this crap to find if we are at the end of an indent tag
1327
# All this crap to find if we are at the end of an indent tag
1326
1328
if insert_iter.ends_tag():
1327
1329
for t in insert_iter.get_toggled_tags(False):
1328
1330
if t.get_data('is_indent'):
1329
1331
self.deindent(insert_iter)
1330
1332
tv.emit_stop_by_name('backspace')
1331
#we stopped the signal, don't forget to erase
1332
#the selection if one
1333
# we stopped the signal, don't forget to erase
1334
# the selection if one
1333
1335
self.buff.delete_selection(True, True)
1334
1336
self.insert_sigid = self.buff.connect('insert-text',
1335
self._insert_at_cursor)
1337
self._insert_at_cursor)
1337
#The mouse is moving. We must change it to a hand when hovering over a link
1339
# The mouse is moving. We must change it to a hand when hovering over a
1338
1341
def _motion(self, view, ev):
1339
1342
window = ev.window
1340
1343
x, y, _ = window.get_pointer()
1367
1370
self.open_task(anchor)
1368
1371
elif typ == "http":
1369
1372
if button == 1 and self.check_link(anchor) and \
1370
not self.buff.get_has_selection():
1373
not self.buff.get_has_selection():
1371
1374
openurl(anchor)
1373
print "Unknown link type for %s" %anchor
1376
print "Unknown link type for %s" % anchor
1374
1377
self.emit('anchor-clicked', text, anchor, button)
1375
1378
self.__set_anchor(ev.window, tag, cursor,
1376
self.get_property('hover'))
1379
self.get_property('hover'))
1377
1380
elif button in [1, 2]:
1378
1381
self.__set_anchor(ev.window, tag, cursor,
1379
self.get_property('active'))
1382
self.get_property('active'))
1381
1384
def __tag_reset(self, tag, window):
1382
1385
if tag.get_data('is_anchor'):
1383
#We need to get the normal cursor back
1386
# We need to get the normal cursor back
1384
1387
editing_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
1385
1388
if tag.get_property('strikethrough'):
1386
1389
linktype = 'done'