1
# -*- coding: utf-8 -*-
2
# Copyright © 2005 Lateef Alabi-Oki
4
# This file is part of Scribes.
6
# Scribes is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
11
# Scribes is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with Scribes; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
22
This module documents a class that creates an object that searches for,
23
or replaces, text found in the text editor's buffer.
25
@author: Lateef Alabi-Oki
26
@organization: The Scribes Project
27
@copyright: Copyright © 2005 Lateef Alabi-Oki
28
@license: GNU GPLv2 or Later
29
@contact: mystilleef@gmail.com
32
from gobject import GObject, SIGNAL_RUN_LAST, TYPE_NONE
34
class SearchReplaceManager(GObject):
36
This class implements and object that searches for, or replaces,
37
text found in the text editor's buffer.
41
"matches-found": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
42
"no-matches-found": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
43
"searching": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
44
"next": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
45
"previous": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
46
"cancel": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
47
"updated-queries": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
48
"replacing": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
49
"replaced": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
50
"replaced-all": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
51
"destroy": (SIGNAL_RUN_LAST, TYPE_NONE, ()),
54
def __init__(self, editor):
56
Initialize the object.
58
@param self: Reference to the SearchReplaceManager instance.
59
@type self: A SearchReplaceManager object.
61
@param editor: Reference to the text editor.
62
@type editor: An Editor object.
64
GObject.__init__(self)
65
self.__init_attributes(editor)
66
self.__store_id = self.__editor.store.add_object("SearchReplaceManager", self)
67
self.__signal_id_1 = self.connect("searching", self.__search_searching_cb)
68
self.__signal_id_2 = self.connect("matches-found", self.__search_matches_found_cb)
69
self.__signal_id_3 = self.connect("no-matches-found", self.__search_no_matches_found_cb)
70
self.__signal_id_4 = self.connect("next", self.__search_next_cb)
71
self.__signal_id_5 = self.connect("previous", self.__search_previous_cb)
72
self.__signal_id_6 = self.connect("cancel", self.__search_cancel_cb)
73
self.__signal_id_7 = self.connect("replacing", self.__search_replacing_cb)
74
self.__signal_id_8 = self.connect("replaced", self.__search_replaced_cb)
75
self.__signal_id_9 = self.connect("replaced-all", self.__search_replace_all_cb)
76
self.__signal_id_10 = editor.connect("hide-bar", self.__search_hide_bar_cb)
77
self.__signal_id_11 = editor.connect("show-bar", self.__search_show_bar_cb)
78
self.__signal_id_12 = self.connect("destroy", self.__destroy_cb)
79
self.__gconf_client.notify_add("/apps/scribes/match_case", self.__search_client_cb)
80
self.__gconf_client.notify_add("/apps/scribes/match_word", self.__search_client_cb)
82
def __init_attributes(self, editor):
84
Initialize data attributes for the object.
86
@param self: Reference to the SearchReplaceManager instance.
87
@type self: A SearchReplaceManager object.
89
@param editor: Reference to the text editor.
90
@type editor: An Editor object.
92
self.__editor = editor
94
self.__start_point_mark = None
95
self.__end_point_mark = None
97
self.__number_of_matches = None
98
self.__position_of_matches = []
99
self.__match_tag = self.__create_match_tag()
100
self.__selection_tag = self.__create_selection_tag()
101
self.__replace_tag = self.__create_replace_tag()
102
self.__cancel_search_operation = False
103
self.__cancel_replace_operation = False
104
self.__enable_regular_expression = False
105
self.__enable_incremental_search = False
106
self.__reset_flag = True
107
from gconf import client_get_default
108
self.__gconf_client = client_get_default()
109
self.__match_case = self.__gconf_client.get_bool("/apps/scribes/match_case")
110
self.__match_word = self.__gconf_client.get_bool("/apps/scribes/match_word")
111
self.__is_initialized = True
112
self.__bar_is_visible = False
113
self.__status_id = None
114
self.__signal_id_1 = self.__signal_id_2 = self.__signal_id_3 = None
115
self.__signal_id_4 = self.__signal_id_5 = self.__signal_id_6 = None
116
self.__signal_id_7 = self.__signal_id_8 = self.__signal_id_9 = None
117
self.__signal_id_10 = self.__signal_id_11 = self.__signal_id_12 = None
118
self.__store_id = None
121
def __get_index(self):
124
def __get_number_of_matches(self):
125
return self.__number_of_matches
127
def __get_queries(self):
128
return self.__queries
130
def __get_is_initialized(self):
131
return self.__is_initialized
133
########################################################################
135
# Public Search Methods
137
########################################################################
139
# Public API property.
140
index = property(__get_index, doc="")
141
number_of_matches = property(__get_number_of_matches, doc="Number of match found")
142
queries = property(__get_queries, doc="")
143
is_initialized = property(__get_is_initialized, doc="")
145
def find(self, string, start, end):
147
Search the text editor's buffer for a string.
149
@param self: Reference to the SearchReplaceManager instance.
150
@type self: A SearchReplaceManager object.
152
@param string: A string in the buffer to search for.
153
@type string: A String object.
155
self.__start_point_mark = start
156
self.__end_point_mark = end
157
self.__reset_flag = False
158
self.__cancel_search_operation = False
159
self.emit("searching")
160
if not string in self.__queries:
161
self.__queries.append(string)
162
self.emit("updated-queries")
164
self.__queries.remove(string)
165
self.__queries.append(string)
166
# begin, end = self.__editor.textbuffer.get_bounds()
167
found_matches = self.__search_for_matches(string)
168
if self.__cancel_search_operation:
172
self.__position_of_matches = found_matches
173
self.emit("matches-found")
175
if self.__cancel_search_operation:
178
self.emit("no-matches-found")
181
def search(self, string, start, end):
182
self.find(string, start, end)
187
Move the selection tag to the next match.
189
@param self: Reference to the SearchReplaceManager instance.
190
@type self: A SearchReplaceManager object.
197
Move the selection tag to the previous match.
199
@param self: Reference to the SearchReplaceManager instance.
200
@type self: A SearchReplaceManager object.
202
self.emit("previous")
205
########################################################################
207
# Public Replace Methods
209
########################################################################
211
def replace(self, text):
213
Replace selected text in the text editor's buffer.
215
@param self: Reference to the Replace instance.
216
@type self: A Replace object.
218
self.__reset_flag = False
219
self.__cancel_replace_operation = False
220
self.emit("replacing")
221
begin, end = self.__position_of_matches[self.__index]
222
textbuffer = self.__editor.textbuffer
223
begin_mark = textbuffer.create_mark(None, begin, True)
225
textbuffer.begin_user_action()
226
textbuffer.delete(begin, end)
227
begin = textbuffer.get_iter_at_mark(begin_mark)
228
textbuffer.insert_with_tags(begin, text, self.__replace_tag)
229
textbuffer.end_user_action()
230
if begin_mark.get_deleted() is False:
231
textbuffer.delete_mark(begin_mark)
232
self.emit("replaced")
235
def replace_all(self, text):
237
Replace all selected text in the text editor's buffer.
239
@param self: Reference to the Replace instance.
240
@type self: A Replace object.
242
self.__reset_flag = False
243
self.__cancel_replace_operation = False
244
self.emit("replacing")
246
textbuffer = self.__editor.textbuffer
248
for item in position_marks:
249
if item[0].get_deleted() is False:
250
textbuffer.delete_mark(item[0])
251
if item[1].get_deleted() is False:
252
textbuffer.delete_mark(item[1])
253
from SCRIBES.utils import response
254
for position in self.__position_of_matches:
256
mark_begin = textbuffer.create_mark(None, position[0], True)
257
mark_end = textbuffer.create_mark(None, position[1], False)
258
position_marks.append((mark_begin, mark_end))
259
if self.__cancel_replace_operation:
264
from SCRIBES.cursor import move_view_to_cursor
265
textbuffer.begin_user_action()
266
for position in position_marks:
268
if self.__cancel_replace_operation:
271
begin = textbuffer.get_iter_at_mark(position[0])
272
end = textbuffer.get_iter_at_mark(position[1])
273
textbuffer.delete(begin, end)
274
begin = textbuffer.get_iter_at_mark(position[0])
275
textbuffer.place_cursor(begin)
276
textbuffer.insert_with_tags(begin, text, self.__replace_tag)
277
move_view_to_cursor(self.__editor.textview)
278
textbuffer.end_user_action()
280
if self.__cancel_replace_operation:
283
self.emit("replaced-all")
286
########################################################################
290
########################################################################
294
Cancel a search operation.
296
@param self: Reference to the SearchReplaceManager instance.
297
@type self: A SearchReplaceManager object.
304
Reset the search processor's state.
306
@param self: Reference to the SearchReplaceManager instance.
307
@type self: A SearchReplaceManager object.
312
def enable_incremental_searching(self, value=True):
314
Enable or disable incremental searching.
316
@param self: Reference to the SearchReplaceManager instance.
317
@type self: A SearchReplaceManager object.
319
@param value: True to enable incremental searching.
320
@type value: A Boolean object.
322
if value is True or value is False:
323
self.__enable_incremental_search = value
326
def get_queries(self):
328
Return a list of search queries.
330
@param self: Reference to the SearchReplaceManager instance.
331
@type self: A SearchReplaceManager object.
333
@return: A list of search queries.
334
@rtype: A List object.
336
return self.__queries
338
def is_initialized(self):
339
return self.__is_initialized
341
########################################################################
345
########################################################################
347
def __create_match_tag(self):
349
Create the search processor's match tag.
351
@param self: Reference to the SearchReplaceManager instance.
352
@type self: A SearchReplaceManager object.
354
tag = self.__editor.textbuffer.create_tag()
355
tag.set_property("background", "yellow")
356
tag.set_property("foreground", "blue")
357
from pango import WEIGHT_BOLD
358
tag.set_property("weight", WEIGHT_BOLD)
361
def __create_selection_tag(self):
363
Create the search processor's selection tag
365
@param self: Reference to the SearchReplaceManager instance.
366
@type self: A SearchReplaceManager object.
368
tag = self.__editor.textbuffer.create_tag()
369
tag.set_property("background", "blue")
370
tag.set_property("foreground", "yellow")
371
from pango import WEIGHT_HEAVY
372
tag.set_property("weight", WEIGHT_HEAVY)
375
def __create_replace_tag(self):
377
Create the replace tag for the Replace object.
379
@param self: Reference to the Replace instance.
380
@type self: A Replace object.
382
tag = self.__editor.textbuffer.create_tag()
383
tag.set_property("background", "green")
384
tag.set_property("foreground", "blue")
385
from pango import WEIGHT_HEAVY
386
tag.set_property("weight", WEIGHT_HEAVY)
389
########################################################################
393
########################################################################
395
def __search_for_matches(self, string):
397
Search the text editor's buffer for a string.
399
@param self: Reference to the SearchReplaceManager instance.
400
@type self: A SearchReplaceManager object.
402
@param string: The string to search for.
403
@type string: A String object.
405
begin = self.__editor.textbuffer.get_iter_at_mark(self.__start_point_mark)
406
end = self.__editor.textbuffer.get_iter_at_mark(self.__end_point_mark)
407
if self.__enable_incremental_search:
408
from SCRIBES.cursor import get_cursor_iterator
409
begin = get_cursor_iterator(self.__editor.textbuffer)
410
self.__editor.textbuffer.move_mark(self.__start_point_mark, begin)
411
text = self.__editor.textbuffer.get_text(begin, end)
413
from re import UNICODE, findall, escape, MULTILINE, IGNORECASE
414
if self.__enable_regular_expression:
415
if self.__match_case:
416
matches = findall(string, text, UNICODE|MULTILINE)
418
matches = findall(string, text, UNICODE|MULTILINE|IGNORECASE)
420
if self.__match_case:
421
matches = findall(escape(string), text, UNICODE|MULTILINE)
423
matches = findall(escape(string), text, UNICODE|MULTILINE|IGNORECASE)
426
found_matches = self.__get_positions(matches)
429
def __get_positions(self, matches):
431
Get position of found matches in the buffer.
433
@param self: Reference to the SearchReplaceManager instance.
434
@type self: A SearchReplaceManager object.
436
@param matches: Found matches.
437
@type matches: A List object.
439
@return: Position of found matches in the text editor's buffer.
440
@rtype: A List object.
443
begin = self.__editor.textbuffer.get_iter_at_mark(self.__start_point_mark)
444
end = self.__editor.textbuffer.get_iter_at_mark(self.__end_point_mark)
445
from gtk import TEXT_SEARCH_VISIBLE_ONLY
446
from SCRIBES.utils import response
447
for match in matches:
448
if self.__cancel_search_operation:
451
start, stop = begin.forward_search(match, TEXT_SEARCH_VISIBLE_ONLY, end)
452
positions.append((start, stop))
454
if self.__match_word and self.__enable_regular_expression is False:
456
for start, stop in positions:
457
if start.starts_word() and stop.ends_word():
458
temp.append((start, stop))
462
########################################################################
464
# Event and Signal Handlers
466
########################################################################
468
def __search_searching_cb(self, SearchReplaceManager):
470
Handles callback when the "searching" signal is emitted.
472
@param self: Reference to the SearchReplaceManager instance.
473
@type self: A SearchReplaceManager object.
475
@param SearchReplaceManager: The text editor's search processor.
476
@type SearchReplaceManager: A SearchReplaceManager object.
478
from i18n import msg0001
480
self.__editor.feedback.unset_modal_message(self.__status_id)
481
self.__status_id = self.__editor.feedback.set_modal_message(msg0001, "run")
484
def __search_matches_found_cb(self, SearchReplaceManager):
486
Handles callback when the "matches-found" signal is emitted.
488
@param self: Reference to the SearchReplaceManager instance.
489
@type self: A SearchReplaceManager object.
491
@param SearchReplaceManager: The text editor's search processor.
492
@type SearchReplaceManager: A SearchReplaceManager object.
494
self.__reset_flag = False
495
if self.__cancel_search_operation:
498
self.__number_of_matches = len(self.__position_of_matches)
499
for position in self.__position_of_matches:
500
if self.__cancel_search_operation:
502
self.__editor.textbuffer.apply_tag(self.__match_tag, position[0], position[1])
503
begin, end = self.__position_of_matches[self.__index]
504
self.__editor.textbuffer.place_cursor(begin)
505
self.__editor.textbuffer.apply_tag(self.__selection_tag, begin, end)
506
from SCRIBES.cursor import move_view_to_cursor
507
move_view_to_cursor(self.__editor.textview)
508
# Send feedback to the status bar.
510
self.__editor.feedback.unset_modal_message(self.__status_id)
511
from i18n import msg0002, msg0003
512
message = msg0003 % (self.__index+1, self.__number_of_matches)
513
self.__status_id = self.__editor.feedback.set_modal_message(message, "find")
514
message = msg0002 % self.__number_of_matches
515
self.__editor.feedback.update_status_message(message, "succeed", 10)
518
def __search_no_matches_found_cb(self, SearchReplaceManager):
520
Handles callback when the "no-matches-found" signal is emitted.
522
@param self: Reference to the SearchReplaceManager instance.
523
@type self: A SearchReplaceManager object.
525
@param SearchReplaceManager: The text editor's SearchReplaceManager.
526
@type SearchReplaceManager: A SearchReplaceManager object.
529
from i18n import msg0004
530
self.__editor.feedback.update_status_message(msg0004, "warning", 10)
533
def __search_next_cb(self, SearchReplaceManager):
535
Handles callback when the "next" signal is emitted.
537
@param self: Reference to the SearchReplaceManager instance.
538
@type self: A SearchReplaceManager object.
540
@param SearchReplaceManager: The text editor's SearchReplaceManager.
541
@type SearchReplaceManager: A SearchReplaceManager object.
543
self.__reset_flag = False
544
if self.__index >= self.__number_of_matches - 1:
545
from i18n import msg0005
546
self.__editor.feedback.update_status_message(msg0005, "warning")
548
begin, end = self.__editor.textbuffer.get_bounds()
549
self.__editor.textbuffer.remove_tag(self.__selection_tag, begin, end)
551
begin, end = self.__position_of_matches[self.__index]
552
self.__editor.textbuffer.place_cursor(begin)
553
self.__editor.textbuffer.apply_tag(self.__selection_tag, begin, end)
554
from SCRIBES.cursor import move_view_to_cursor
555
move_view_to_cursor(self.__editor.textview)
557
self.__editor.feedback.unset_modal_message(self.__status_id)
558
from i18n import msg0003
559
message = msg0003 % (self.__index+1, self.__number_of_matches)
560
self.__status_id = self.__editor.feedback.set_modal_message(message, "find")
563
def __search_previous_cb(self, SearchReplaceManager):
565
Handles callback when the "previous" signal is emitted.
567
@param self: Reference to the SearchReplaceManager instance.
568
@type self: A SearchReplaceManager object.
570
@param SearchReplaceManager: The text editor's SearchReplaceManager.
571
@type SearchReplaceManager: A SearchReplaceManager object.
573
self.__reset_flag = False
574
if self.__index <= 0:
575
from i18n import msg0010
576
self.__editor.feedback.update_status_message(msg0010, "warning")
578
begin, end = self.__editor.textbuffer.get_bounds()
579
self.__editor.textbuffer.remove_tag(self.__selection_tag, begin, end)
581
begin, end = self.__position_of_matches[self.__index]
582
self.__editor.textbuffer.place_cursor(begin)
583
self.__editor.textbuffer.apply_tag(self.__selection_tag, begin, end)
584
from SCRIBES.cursor import move_view_to_cursor
585
move_view_to_cursor(self.__editor.textview)
587
self.__editor.feedback.unset_modal_message(self.__status_id)
588
from i18n import msg0003
589
message = msg0003 % (self.__index+1, self.__number_of_matches)
590
self.__status_id = self.__editor.feedback.set_modal_message(message, "find")
593
def __search_cancel_cb(self, SearchReplaceManager):
595
Handles callback when the "cancel" signal is emitted.
597
@param self: Reference to the SearchReplaceManager instance.
598
@type self: A SearchReplaceManager object.
600
@param SearchReplaceManager: The text editor's SearchReplaceManager.
601
@type SearchReplaceManager: A SearchReplaceManager object.
603
self.__cancel_search_operation = True
604
self.__cancel_replace_operation = True
606
from i18n import msg0006
607
self.__editor.feedback.set_modal_message(msg0006, "stop")
610
def __search_hide_bar_cb(self, editor, bar):
612
Handles callback when the "hide-bar" signal is emitted.
614
@param self: Reference to the SearchReplaceManager instance.
615
@type self: A SearchReplaceManager object.
617
@param editor: Reference to the text editor.
618
@type editor: An Editor object.
620
@param bar: One of the text editor's bar objects.
621
@type bar: A ScribesBar object.
623
from operator import is_, truth
624
if truth(self.__start_point_mark) and is_(self.__start_point_mark.get_deleted(), False):
625
self.__editor.textbuffer.delete_mark(self.__start_point_mark)
626
self.__start_point_mark = None
627
if truth(self.__end_point_mark) and is_(self.__end_point_mark.get_deleted(), False):
628
self.__editor.textbuffer.delete_mark(self.__end_point_mark)
629
self.__end_point_mark = None
630
self.__bar_is_visible = False
631
if not self.__index is None:
632
begin, end = self.__position_of_matches[self.__index]
633
self.__editor.textbuffer.select_range(begin, end)
635
self.__enable_regular_expression = False
636
self.__enable_incremental_search = False
639
def __search_show_bar_cb(self, editor, bar):
641
Handles callback when the "show-bar" signal is emitted.
643
@param self: Reference to the SearchReplaceManager instance.
644
@type self: A SearchReplaceManager object.
646
@param editor: Reference to the text editor.
647
@type editor: An Editor object.
649
@param bar: One of the text editor's bar objects.
650
@type bar: A ScribesBar object.
652
self.__bar_is_visible = True
655
def __search_replacing_cb(self, processor):
656
self.__reset_flag = False
657
from i18n import msg0007
659
self.__editor.feedback.unset_modal_message(self.__status_id)
660
self.__status_id = self.__editor.feedback.set_modal_message(msg0007, "run")
663
def __search_replaced_cb(self, processor):
664
self.__reset_flag = False
665
from i18n import msg0008
667
self.__editor.feedback.unset_modal_message(self.__status_id)
668
self.__editor.feedback.update_status_message(msg0008, "succeed", 10)
671
def __search_replace_all_cb(self, processor):
672
self.__reset_flag = False
673
from i18n import msg0009
675
self.__editor.feedback.unset_modal_message(self.__status_id)
676
self.__editor.feedback.update_status_message(msg0009, "succeed", 10)
679
def __search_client_cb(self, client, cnxn_id, entry, data):
681
Handles callback when the GConf database is modified.
683
@param self: Reference to the SearchReplaceManager instance.
684
@type self: A SearchReplaceManager object.
686
self.__match_case = client.get_bool("/apps/scribes/match_case")
687
self.__match_word = client.get_bool("/apps/scribes/match_word")
690
########################################################################
694
########################################################################
698
Reset the search processor's state.
700
@param self: Reference to the SearchReplaceManager instance.
701
@type self: A SearchReplaceManager object.
703
if self.__reset_flag:
706
self.__editor.feedback.unset_modal_message(self.__status_id)
707
self.__status_id = None
709
self.__number_of_matches = None
710
self.__position_of_matches = []
711
begin, end = self.__editor.textbuffer.get_bounds()
712
self.__editor.textbuffer.remove_tag(self.__match_tag, begin, end)
713
self.__editor.textbuffer.remove_tag(self.__selection_tag, begin, end)
714
self.__editor.textbuffer.remove_tag(self.__replace_tag, begin, end)
715
self.__cancel_search_operation = False
716
self.__cancel_replace_operation = False
717
self.__reset_flag = True
720
def __destroy_cb(self, manager):
722
Handles callback when the "destroy" signal is emitted.
724
@param self: Reference to the SearchReplaceManager instance.
725
@type self: A SearchReplaceManager object.
727
@param manager: Reference to the SearchReplaceManager instance.
728
@type manager: A SearchReplaceManager object.
730
self.__editor.store.remove_object("SearchReplaceManager", self.__store_id)
731
from SCRIBES.utils import disconnect_signal, delete_attributes
732
from SCRIBES.utils import delete_list
733
disconnect_signal(self.__signal_id_1, self)
734
disconnect_signal(self.__signal_id_2, self)
735
disconnect_signal(self.__signal_id_3, self)
736
disconnect_signal(self.__signal_id_4, self)
737
disconnect_signal(self.__signal_id_5, self)
738
disconnect_signal(self.__signal_id_6, self)
739
disconnect_signal(self.__signal_id_7, self)
740
disconnect_signal(self.__signal_id_8, self)
741
disconnect_signal(self.__signal_id_9, self)
742
disconnect_signal(self.__signal_id_12, self)
743
disconnect_signal(self.__signal_id_11, self.__editor)
744
disconnect_signal(self.__signal_id_10, self.__editor)
745
delete_list(self.__queries)
746
delete_list(self.__position_of_matches)
747
delete_attributes(self)