1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2000-2007  Donald N. Allingham
5# Copyright (C) 2011       Tim G L Lyons
6#
7# This program is free software; you can redistribute it and/or modiy
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22#------------------------------------------------------------------------
23#
24# standard python modules
25#
26#------------------------------------------------------------------------
27import pickle
28import os
29from xml.sax.saxutils import escape
30from time import strftime as strftime
31
32#-------------------------------------------------------------------------
33#
34# GTK/Gnome modules
35#
36#-------------------------------------------------------------------------
37from gi.repository import GObject
38from gi.repository import Gdk
39from gi.repository import Gtk
40from gi.repository import GdkPixbuf
41from gi.repository import Pango
42import cairo
43
44#-------------------------------------------------------------------------
45#
46# gramps modules
47#
48#-------------------------------------------------------------------------
49from gramps.gen.const import URL_MANUAL_PAGE
50from gramps.gen.lib import NoteType
51from gramps.gen.datehandler import get_date
52from gramps.gen.display.place import displayer as place_displayer
53from gramps.gen.errors import WindowActiveError
54from gramps.gen.constfunc import mac
55from .display import display_help
56from .managedwindow import ManagedWindow
57from .glade import Glade
58from .ddtargets import DdTargets
59from .makefilter import make_filter
60from .utils import is_right_click, no_match_primary_mask
61from gramps.gen.const import GRAMPS_LOCALE as glocale
62_ = glocale.translation.sgettext
63
64#-------------------------------------------------------------------------
65#
66# Constants
67#
68#-------------------------------------------------------------------------
69WIKI_HELP_PAGE = '%s_-_Navigation' % URL_MANUAL_PAGE
70WIKI_HELP_SEC = _('manual|Using_the_Clipboard')
71clipdb = None  # current db to avoid different transient dbs during db change
72
73#-------------------------------------------------------------------------
74#
75# icons used in the object listing
76#
77#-------------------------------------------------------------------------
78
79theme = Gtk.IconTheme.get_default()
80LINK_PIC = theme.load_icon('stock_link', 16, 0)
81ICONS = {}
82for (name, icon) in (("media", "gramps-media"),
83                     ("note", "gramps-notes"),
84                     ("person", "gramps-person"),
85                     ("place", "gramps-place"),
86                     ('address', 'gramps-address'),
87                     ('attribute', 'gramps-attribute'),
88                     ('event', 'gramps-event'),
89                     ('family', 'gramps-family'),
90                     ('location', 'geo-place-link'),
91                     ('media', 'gramps-media'),
92                     ('name', 'geo-show-person'),
93                     ('repository', 'gramps-repository'),
94                     ('source', 'gramps-source'),
95                     ('citation', 'gramps-citation'),
96                     ('text', 'gramps-font'),
97                     ('url', 'gramps-geo')):
98    ICONS[name] = theme.load_icon(icon, 16, 0)
99
100
101#-------------------------------------------------------------------------
102#
103# Local functions
104#
105#-------------------------------------------------------------------------
106def map2class(target):
107    _d_ = {"person-link": ClipPersonLink,
108           "family-link": ClipFamilyLink,
109           'personref': ClipPersonRef,
110           'childref': ClipChildRef,
111           'source-link': ClipSourceLink,
112           'citation-link': ClipCitation,
113           'repo-link': ClipRepositoryLink,
114           'pevent': ClipEvent,
115           'eventref': ClipEventRef,
116           'media': ClipMediaObj,
117           'mediaref': ClipMediaRef,
118           'place-link': ClipPlace,
119           'placeref': ClipPlaceRef,
120           'note-link': ClipNote,
121           'TEXT': ClipText}
122    return _d_[target] if target in _d_ else None
123
124
125def obj2class(target):
126    _d_ = {"Person": ClipPersonLink,
127           "Family": ClipFamilyLink,
128           'Source': ClipSourceLink,
129           'Citation': ClipCitation,
130           'Repository': ClipRepositoryLink,
131           'Event': ClipEvent,
132           'Media': ClipMediaObj,
133           'Place': ClipPlace,
134           'Note': ClipNote}
135    return _d_[target] if target in _d_ else None
136
137OBJ2TARGET = {"Person": Gdk.atom_intern('person-link', False),
138              "Family": Gdk.atom_intern('family-link', False),
139              'Source': Gdk.atom_intern('source-link', False),
140              'Citation': Gdk.atom_intern('citation-link', False),
141              'Repository': Gdk.atom_intern('repo-link', False),
142              'Event': Gdk.atom_intern('pevent', False),
143              'Media': Gdk.atom_intern('media', False),
144              'Place': Gdk.atom_intern('place-link', False),
145              'Note': Gdk.atom_intern('note-link', False)}
146
147
148def obj2target(target):
149    return OBJ2TARGET[target] if target in OBJ2TARGET else None
150
151
152def model_contains(model, data):
153    """
154    Returns True if data is a row in model.
155    """
156    # check type and value
157    # data[0] is type of drop item, data[1] is Clip object
158    for row in model:
159        if data[0] == 'TEXT':
160            same = ((row[0] == data[0]) and
161                    (row[1]._value == data[1]._value))
162        else:
163            # FIXME: too restrictive, birth and death won't both copy
164            same = ((row[0] == data[0]) and
165                    (row[1]._title == data[1]._title) and
166                    (row[1]._handle == data[1]._handle) and
167                    (row[3] == data[3]) and
168                    (row[4] == data[4]))
169        if same:
170            return True
171    return False
172
173
174#-------------------------------------------------------------------------
175#
176# wrapper classes to provide object specific listing in the ListView
177#
178#-------------------------------------------------------------------------
179class ClipWrapper:
180    UNAVAILABLE_ICON = 'dialog-error'
181
182    def __init__(self, obj):
183        self._obj = obj
184        self._pickle = obj
185        self._type = _("Unknown")
186        self._objclass = None
187        self._handle = None
188        self._title = _('Unavailable')
189        self._value = _('Unavailable')
190        self._dbid = clipdb.get_dbid()
191        self._dbname = clipdb.get_dbname()
192
193    def get_type(self):
194        return self._type
195
196    def get_title(self):
197        return self._title
198
199    def get_value(self):
200        return self._value
201
202    def get_dbname(self):
203        return self._dbname
204
205    def pack(self):
206        """
207        Return a byte string that can be packed in a GtkSelectionData
208        structure
209        """
210        if not self.is_valid():
211            data = list(pickle.loads(self._pickle))
212            data[2] = {
213                "_obj": self._obj,
214                "_pickle": self._pickle,
215                "_type": self._type,
216                "_handle": self._handle,
217                "_objclass": self._objclass,
218                "_title": self._title,
219                "_value": self._value,
220                "_dbid": self._dbid,
221                "_dbname": self._dbname}
222            return pickle.dumps(data)
223        if isinstance(self._obj, bytes):
224            return self._obj
225        # don't know if this happens in Gramps, theoretically possible
226        asuni = str(self._obj)
227        return asuni.encode('utf-8')
228
229    def is_valid(self):
230        if self._objclass and obj2class(self._objclass):
231            # we have a primary object
232            data = pickle.loads(self._obj)
233            handle = data[2]
234            return clipdb.method("has_%s_handle", self._objclass)(handle)
235        return True
236
237
238class ClipHandleWrapper(ClipWrapper):
239
240    def __init__(self, obj):
241        super(ClipHandleWrapper, self).__init__(obj)
242        # unpack object
243        (drag_type, idval, data, val) = pickle.loads(obj)
244        if isinstance(data, dict):
245            self.set_data(data)
246        else:
247            self._handle = data
248
249    def set_data(self, data):
250        for item in data:
251            setattr(self, item, data[item])
252
253
254class ClipObjWrapper(ClipWrapper):
255
256    def __init__(self, obj):
257        super(ClipObjWrapper, self).__init__(obj)
258        #unpack object
259        (drag_type, idval, self._obj, val) = pickle.loads(obj)
260        self._pickle = obj
261
262    def pack(self):
263        if not self.is_valid():
264            data = list(pickle.loads(self._pickle))
265            data[2] = {
266                "_obj": self._obj,
267                "_pickle": self._pickle,
268                "_type": self._type,
269                "_handle": self._handle,
270                "_objclass": self._objclass,
271                "_title": self._title,
272                "_value": self._value,
273                "_dbid": self._dbid,
274                "_dbname": self._dbname}
275            return pickle.dumps(data)
276        return self._pickle
277
278    def is_valid(self):
279        if self._obj is None:
280            return False
281
282        for (clname, handle) in self._obj.get_referenced_handles_recursively():
283            if obj2class(clname):  # a class we care about (not tag)
284                if not clipdb.method("has_%s_handle", clname)(handle):
285                    return False
286
287        return True
288
289
290class ClipAddress(ClipObjWrapper):
291
292    DROP_TARGETS = [DdTargets.ADDRESS]
293    DRAG_TARGET = DdTargets.ADDRESS
294    ICON = ICONS['address']
295
296    def __init__(self, obj):
297        super(ClipAddress, self).__init__(obj)
298        self._type = _("Address")
299        self.refresh()
300
301    def refresh(self):
302        if self._obj:
303            self._title = get_date(self._obj)
304            self._value = "%s %s %s %s" % (self._obj.get_street(),
305                                           self._obj.get_city(),
306                                           self._obj.get_state(),
307                                           self._obj.get_country())
308
309
310class ClipLocation(ClipObjWrapper):
311
312    DROP_TARGETS = [DdTargets.LOCATION]
313    DRAG_TARGET = DdTargets.LOCATION
314    ICON = ICONS['location']
315
316    def __init__(self, obj):
317        super(ClipLocation, self).__init__(obj)
318        self._type = _("Location")
319        self.refresh()
320
321    def refresh(self):
322        self._value = "%s %s %s" % (self._obj.get_city(),
323                                    self._obj.get_state(),
324                                    self._obj.get_country())
325
326
327class ClipEvent(ClipHandleWrapper):
328
329    DROP_TARGETS = [DdTargets.EVENT]
330    DRAG_TARGET = DdTargets.EVENT
331    ICON = ICONS["event"]
332
333    def __init__(self, obj):
334        super(ClipEvent, self).__init__(obj)
335        self._type = _("Event")
336        self._objclass = 'Event'
337        self.refresh()
338
339    def refresh(self):
340        if self._handle:
341            value = clipdb.get_event_from_handle(self._handle)
342            if value:
343                self._title = str(value.get_type())
344                self._value = value.get_description()
345
346
347class ClipPlace(ClipHandleWrapper):
348
349    DROP_TARGETS = [DdTargets.PLACE_LINK]
350    DRAG_TARGET = DdTargets.PLACE_LINK
351    ICON = ICONS["place"]
352
353    def __init__(self, obj):
354        super(ClipPlace, self).__init__(obj)
355        self._type = _("Place")
356        self._objclass = 'Place'
357        self.refresh()
358
359    def refresh(self):
360        if self._handle:
361            value = clipdb.get_place_from_handle(self._handle)
362            if value:
363                self._title = value.gramps_id
364                self._value = place_displayer.display(clipdb, value)
365
366
367class ClipNote(ClipHandleWrapper):
368
369    DROP_TARGETS = [DdTargets.NOTE_LINK]
370    DRAG_TARGET = DdTargets.NOTE_LINK
371    ICON = ICONS["note"]
372
373    def __init__(self, obj):
374        super(ClipNote, self).__init__(obj)
375        self._type = _("Note")
376        self._objclass = 'Note'
377        self.refresh()
378
379    def refresh(self):
380        value = clipdb.get_note_from_handle(self._handle)
381        if value:
382            self._title = value.get_gramps_id()
383            note = value.get().replace('\n', ' ')
384            # String must be unicode for truncation to work for non
385            # ascii characters
386            note = str(note)
387            if len(note) > 80:
388                self._value = note[:80] + "..."
389            else:
390                self._value = note
391
392
393class ClipFamilyEvent(ClipObjWrapper):
394
395    DROP_TARGETS = [DdTargets.FAMILY_EVENT]
396    DRAG_TARGET = DdTargets.FAMILY_EVENT
397    ICON = ICONS['family']
398
399    def __init__(self, obj):
400        super(ClipFamilyEvent, self).__init__(obj)
401        self._type = _("Family Event")
402        self.refresh()
403
404    def refresh(self):
405        if self._obj:
406            self._title = str(self._obj.get_type())
407            self._value = self._obj.get_description()
408
409
410class ClipUrl(ClipObjWrapper):
411
412    DROP_TARGETS = [DdTargets.URL]
413    DRAG_TARGET = DdTargets.URL
414    ICON = ICONS['url']
415
416    def __init__(self, obj):
417        super(ClipUrl, self).__init__(obj)
418        self._type = _("Url")
419        self.refresh()
420
421    def refresh(self):
422        if self._obj:
423            self._title = self._obj.get_path()
424            self._value = self._obj.get_description()
425
426
427class ClipAttribute(ClipObjWrapper):
428
429    DROP_TARGETS = [DdTargets.ATTRIBUTE]
430    DRAG_TARGET = DdTargets.ATTRIBUTE
431    ICON = ICONS['attribute']
432
433    def __init__(self, obj):
434        super(ClipAttribute, self).__init__(obj)
435        self._type = _("Attribute")
436        self.refresh()
437
438    def refresh(self):
439        self._title = str(self._obj.get_type())
440        self._value = self._obj.get_value()
441
442
443class ClipFamilyAttribute(ClipObjWrapper):
444
445    DROP_TARGETS = [DdTargets.FAMILY_ATTRIBUTE]
446    DRAG_TARGET = DdTargets.FAMILY_ATTRIBUTE
447    ICON = ICONS['attribute']
448
449    def __init__(self, obj):
450        super(ClipFamilyAttribute, self).__init__(obj)
451        self._type = _("Family Attribute")
452        self.refresh()
453
454    def refresh(self):
455        if self._obj:
456            self._title = str(self._obj.get_type())
457            self._value = self._obj.get_value()
458
459
460class ClipCitation(ClipHandleWrapper):
461
462    DROP_TARGETS = [DdTargets.CITATION_LINK]
463    DRAG_TARGET = DdTargets.CITATION_LINK
464    ICON = ICONS["citation"]
465
466    def __init__(self, obj):
467        super(ClipCitation, self).__init__(obj)
468        self._type = _("Citation")
469        self._objclass = 'Citation'
470        self.refresh()
471
472    def refresh(self):
473        if self._handle:
474            try:
475                citation = clipdb.get_citation_from_handle(self._handle)
476                if citation:
477                    self._title = citation.get_gramps_id()
478                    notelist = list(map(clipdb.get_note_from_handle,
479                                        citation.get_note_list()))
480                    srctxtlist = [note for note in notelist
481                                  if note.get_type() == NoteType.SOURCE_TEXT]
482                    page = citation.get_page()
483                    if not page:
484                        page = _('not available|NA')
485                    text = ""
486                    if srctxtlist:
487                        text = " ".join(srctxtlist[0].get().split())
488                    #String must be unicode for truncation to work for non
489                    #ascii characters
490                        text = str(text)
491                        if len(text) > 60:
492                            text = text[:60] + "..."
493                    self._value = _("Volume/Page: %(pag)s -- %(sourcetext)s"
494                                   ) % { 'pag' : page,
495                                         'sourcetext' : text}
496            except:
497                # We are in the Source tree view. The shortcuts only
498                # work for citations.
499                print("We cannot copy the source from this view."
500                      " Use drag and drop.")
501                self._title = self._value = ''
502                self._pickle = self._type = self._objclass = None
503                self._handle = self._dbid = self._dbname = None
504
505
506class ClipRepoRef(ClipObjWrapper):
507
508    DROP_TARGETS = [DdTargets.REPOREF]
509    DRAG_TARGET = DdTargets.REPOREF
510    ICON = LINK_PIC
511
512    def __init__(self, obj):
513        super(ClipRepoRef, self).__init__(obj)
514        self._type = _("Repository ref")
515        self.refresh()
516
517    def refresh(self):
518        if self._obj:
519            base = clipdb.get_repository_from_handle(self._obj.ref)
520            if base:
521                self._title = str(base.get_type())
522                self._value = base.get_name()
523
524
525class ClipEventRef(ClipObjWrapper):
526
527    DROP_TARGETS = [DdTargets.EVENTREF]
528    DRAG_TARGET = DdTargets.EVENTREF
529    ICON = LINK_PIC
530
531    def __init__(self, obj):
532        super(ClipEventRef, self).__init__(obj)
533        self._type = _("Event ref")
534        self.refresh()
535
536    def refresh(self):
537        if self._obj:
538            base = clipdb.get_event_from_handle(self._obj.ref)
539            if base:
540                self._title = base.gramps_id
541                self._value = str(base.get_type())
542
543
544class ClipPlaceRef(ClipObjWrapper):
545
546    DROP_TARGETS = [DdTargets.PLACEREF]
547    DRAG_TARGET = DdTargets.PLACEREF
548    ICON = LINK_PIC
549
550    def __init__(self, obj):
551        super(ClipPlaceRef, self).__init__(obj)
552        self._type = _("Place ref")
553        self.refresh()
554
555    def refresh(self):
556        if self._obj:
557            base = clipdb.get_place_from_handle(self._obj.ref)
558            if base:
559                self._title = base.gramps_id
560                self._value = place_displayer.display(clipdb, base)
561
562
563class ClipName(ClipObjWrapper):
564
565    DROP_TARGETS = [DdTargets.NAME]
566    DRAG_TARGET = DdTargets.NAME
567    ICON = ICONS['name']
568
569    def __init__(self, obj):
570        super(ClipName, self).__init__(obj)
571        self._type = _("Name")
572        self.refresh()
573
574    def refresh(self):
575        if self._obj:
576            self._title = str(self._obj.get_type())
577            self._value = self._obj.get_name()
578
579
580class ClipPlaceName(ClipObjWrapper):
581
582    DROP_TARGETS = [DdTargets.PLACENAME]
583    DRAG_TARGET = DdTargets.PLACENAME
584    ICON = ICONS['name']
585
586    def __init__(self, obj):
587        super(ClipPlaceName, self).__init__(obj)
588        self._type = _("Place Name")
589        self.refresh()
590
591    def refresh(self):
592        if self._obj:
593            self._title = get_date(self._obj)
594            self._value = self._obj.get_value()
595
596
597class ClipSurname(ClipObjWrapper):
598
599    DROP_TARGETS = [DdTargets.SURNAME]
600    DRAG_TARGET = DdTargets.SURNAME
601    ICON = ICONS['name']
602
603    def __init__(self, obj):
604        super(ClipSurname, self).__init__(obj)
605        self._type = _("Surname")
606        self.refresh()
607
608    def refresh(self):
609        if self._obj:
610            self._title = self._obj.get_surname()
611            self._value = self._obj.get_surname()
612
613
614class ClipText(ClipWrapper):
615
616    DROP_TARGETS = DdTargets.all_text()
617    DRAG_TARGET = DdTargets.TEXT
618    ICON = ICONS['text']
619
620    def __init__(self, obj):
621        super(ClipText, self).__init__(obj)
622        self._type = _("Text")
623        if isinstance(self._obj, bytes):
624            self._pickle = str(self._obj, "utf-8")
625        else:
626            self._pickle = self._obj
627        self.refresh()
628
629    def refresh(self):
630        self._title = _("Text")
631        if isinstance(self._obj, bytes):
632            self._value = str(self._obj, "utf-8")
633        else:
634            self._value = self._obj
635
636
637class ClipMediaObj(ClipHandleWrapper):
638
639    DROP_TARGETS = [DdTargets.MEDIAOBJ]
640    DRAG_TARGET = DdTargets.MEDIAOBJ
641    ICON = ICONS["media"]
642
643    def __init__(self, obj):
644        super(ClipMediaObj, self).__init__(obj)
645        self._type = _("Media")
646        self._objclass = 'Media'
647        self.refresh()
648
649    def refresh(self):
650        if self._handle:
651            obj = clipdb.get_media_from_handle(self._handle)
652            if obj:
653                self._title = obj.get_description()
654                self._value = obj.get_path()
655
656
657class ClipMediaRef(ClipObjWrapper):
658
659    DROP_TARGETS = [DdTargets.MEDIAREF]
660    DRAG_TARGET = DdTargets.MEDIAREF
661    ICON = LINK_PIC
662
663    def __init__(self, obj):
664        super(ClipMediaRef, self).__init__(obj)
665        self._type = _("Media ref")
666        self.refresh()
667
668    def refresh(self):
669        if self._obj:
670            base = clipdb.get_media_from_handle(
671                self._obj.get_reference_handle())
672            if base:
673                self._title = base.get_description()
674                self._value = base.get_path()
675
676
677class ClipPersonRef(ClipObjWrapper):
678
679    DROP_TARGETS = [DdTargets.PERSONREF]
680    DRAG_TARGET = DdTargets.PERSONREF
681    ICON = LINK_PIC
682
683    def __init__(self, obj):
684        super(ClipPersonRef, self).__init__(obj)
685        self._type = _("Person ref")
686        self.refresh()
687
688    def refresh(self):
689        if self._obj:
690            person = clipdb.get_person_from_handle(
691                self._obj.get_reference_handle())
692            if person:
693                self._title = self._obj.get_relation()
694                self._value = person.get_primary_name().get_name()
695
696
697class ClipChildRef(ClipObjWrapper):
698
699    DROP_TARGETS = [DdTargets.CHILDREF]
700    DRAG_TARGET = DdTargets.CHILDREF
701    ICON = LINK_PIC
702
703    def __init__(self, obj):
704        super(ClipChildRef, self).__init__(obj)
705        self._type = _("Child ref")
706        self.refresh()
707
708    def refresh(self):
709        if self._obj:
710            person = clipdb.get_person_from_handle(
711                self._obj.get_reference_handle())
712            if person:
713                frel = str(self._obj.get_father_relation())
714                mrel = str(self._obj.get_mother_relation())
715                self._title = _('%(frel)s %(mrel)s') % {'frel': frel,
716                                                        'mrel': mrel}
717                self._value = person.get_primary_name().get_name()
718
719
720class ClipPersonLink(ClipHandleWrapper):
721
722    DROP_TARGETS = [DdTargets.PERSON_LINK]
723    DRAG_TARGET = DdTargets.PERSON_LINK
724    ICON = ICONS["person"]
725
726    def __init__(self, obj):
727        super(ClipPersonLink, self).__init__(obj)
728        self._type = _("Person")
729        self._objclass = 'Person'
730        self.refresh()
731
732    def refresh(self):
733        if self._handle:
734            person = clipdb.get_person_from_handle(self._handle)
735            if person:
736                self._title = person.gramps_id
737                self._value = person.get_primary_name().get_name()
738
739
740class ClipFamilyLink(ClipHandleWrapper):
741
742    DROP_TARGETS = [DdTargets.FAMILY_LINK]
743    DRAG_TARGET = DdTargets.FAMILY_LINK
744    ICON = ICONS["family"]
745
746    def __init__(self, obj):
747        super(ClipFamilyLink, self).__init__(obj)
748        self._type = _("Family")
749        self._objclass = 'Family'
750        self.refresh()
751
752    def refresh(self):
753        from gramps.gen.simple import SimpleAccess
754        if self._handle:
755            family = clipdb.get_family_from_handle(self._handle)
756            if family:
757                _sa = SimpleAccess(clipdb)
758                self._title = family.gramps_id
759                self._value = _sa.describe(family)
760
761
762class ClipSourceLink(ClipHandleWrapper):
763
764    DROP_TARGETS = [DdTargets.SOURCE_LINK]
765    DRAG_TARGET = DdTargets.SOURCE_LINK
766    ICON = ICONS["source"]
767
768    def __init__(self, obj):
769        super(ClipSourceLink, self).__init__(obj)
770        self._type = _("Source")
771        self._objclass = 'Source'
772        self.refresh()
773
774    def refresh(self):
775        if self._handle:
776            source = clipdb.get_source_from_handle(self._handle)
777            if source:
778                self._title = source.get_gramps_id()
779                self._value = source.get_title()
780
781
782class ClipRepositoryLink(ClipHandleWrapper):
783
784    DROP_TARGETS = [DdTargets.REPO_LINK]
785    DRAG_TARGET = DdTargets.REPO_LINK
786    ICON = ICONS["repository"]
787
788    def __init__(self, obj):
789        super(ClipRepositoryLink, self).__init__(obj)
790        self._type = _("Repository")
791        self._objclass = 'Repository'
792        self.refresh()
793
794    def refresh(self):
795        if self._handle:
796            source = clipdb.get_repository_from_handle(self._handle)
797            if source:
798                self._title = str(source.get_type())
799                self._value = source.get_name()
800
801#-------------------------------------------------------------------------
802#
803# Wrapper classes to deal with lists of objects
804#
805#-------------------------------------------------------------------------
806
807
808class ClipDropList:
809    DROP_TARGETS = [DdTargets.LINK_LIST]
810    DRAG_TARGET = None
811
812    def __init__(self, obj_list):
813        # ('link-list', id, (('person-link', handle),
814        #                    ('person-link', handle), ...), 0)
815        self._obj_list = pickle.loads(obj_list)
816
817    def get_objects(self):
818        list_type, _id, handles, timestamp = self._obj_list
819        retval = []
820        for (target, handle) in handles:
821            _class = map2class(target)
822            if _class:
823                obj = _class(pickle.dumps((target, _id, handle, timestamp)))
824                if obj:
825                    retval.append(obj)
826        return retval
827
828
829class ClipDropRawList(ClipDropList):
830    DROP_TARGETS = [DdTargets.RAW_LIST]
831    DRAG_TARGET = None
832
833    def __init__(self, obj_list):
834        # ('raw-list', id, (ClipObject, ClipObject, ...), 0)
835        self._obj_list = pickle.loads(obj_list)
836
837    def get_objects(self):
838        retval = []
839        for item in self._obj_list:
840            if item is None:
841                continue
842            target = pickle.loads(item)[0]
843            _class = map2class(target)
844            if _class:
845                obj = _class(item)
846                if obj:
847                    retval.append(obj)
848        return retval
849
850
851class ClipDropHandleList(ClipDropList):
852    DROP_TARGETS = [DdTargets.HANDLE_LIST]
853    DRAG_TARGET = None
854
855    def __init__(self, obj_list):
856        # incoming:
857        # ('handle-list', id, (('Person', '2763526751235'),
858        #                      ('Source', '3786234743978'), ...), 0)
859        self._obj_list = pickle.loads(obj_list)
860
861    def get_objects(self):
862        retval = []
863        for (objclass, handle) in self._obj_list:
864            _class = obj2class(objclass)
865            target = obj2target(objclass).name()
866            # outgoing:
867            # (drag_type, idval, self._handle, val) = pickle.loads(self._obj)
868            data = (target, id(self), handle, 0)
869            obj = _class(pickle.dumps(data))
870            retval.append(obj)
871        return retval
872
873# FIXME: add family
874
875
876#-------------------------------------------------------------------------
877#
878# ClipboardListModel class
879#
880#-------------------------------------------------------------------------
881class ClipboardListModel(Gtk.ListStore):
882
883    def __init__(self):
884        Gtk.ListStore.__init__(self,
885                               str,     # 0: object type
886                               object,  # 1: object
887                               object,  # 2: tooltip callback
888                               str,     # 3: type
889                               str,     # 4: value
890                               str,     # 5: unique database id (dbid)
891                               str)     # 6: db name (may be old)
892
893
894#-------------------------------------------------------------------------
895#
896# ClipboardListView class
897#
898#-------------------------------------------------------------------------
899class ClipboardListView:
900
901    LOCAL_DRAG_TYPE = 'MY_TREE_MODEL_ROW'
902    LOCAL_DRAG_TARGET = Gtk.TargetEntry.new(LOCAL_DRAG_TYPE,
903                                            Gtk.TargetFlags.SAME_WIDGET, 0)
904
905    def __init__(self, dbstate, widget):
906
907        self._widget = widget
908        self.dbstate = dbstate
909        self.dbstate.connect('database-changed', self.database_changed)
910        self.database_changed(dbstate.db)
911
912        self._target_type_to_wrapper_class_map = {}
913        self._previous_drop_time = 0
914
915        # Create the tree columns
916        self._col1 = Gtk.TreeViewColumn(_("Type"))
917        self._col1.set_property("resizable", True)
918        self._col1.set_sort_column_id(0)
919        self._col2 = Gtk.TreeViewColumn(_("Title"))
920        self._col2.set_property("resizable", True)
921        self._col2.set_sort_column_id(3)
922        self._col3 = Gtk.TreeViewColumn(_("Value"))
923        self._col3.set_property("resizable", True)
924        self._col3.set_sort_column_id(4)
925        self._col4 = Gtk.TreeViewColumn(_("Family Tree"))
926        self._col4.set_property("resizable", True)
927        self._col4.set_sort_column_id(6)
928
929        # Add columns
930        self._widget.append_column(self._col1)
931        self._widget.append_column(self._col2)
932        self._widget.append_column(self._col3)
933        self._widget.append_column(self._col4)
934
935        # Create cell renders
936        self._col1_cellpb = Gtk.CellRendererPixbuf()
937        self._col1_cell = Gtk.CellRendererText()
938        self._col2_cell = Gtk.CellRendererText()
939        self._col3_cell = Gtk.CellRendererText()
940        self._col4_cell = Gtk.CellRendererText()
941
942        # Add cells to view
943        self._col1.pack_start(self._col1_cellpb, False)
944        self._col1.pack_start(self._col1_cell, True)
945        self._col2.pack_start(self._col2_cell, True)
946        self._col3.pack_start(self._col3_cell, True)
947        self._col4.pack_start(self._col4_cell, True)
948
949        # Setup the cell data callback funcs
950        self._col1.set_cell_data_func(self._col1_cellpb, self.object_pixbuf)
951        self._col1.set_cell_data_func(self._col1_cell, self.object_type)
952        self._col2.set_cell_data_func(self._col2_cell, self.object_title)
953        self._col3.set_cell_data_func(self._col3_cell, self.object_value)
954        self._col4.set_cell_data_func(self._col4_cell, self.get_dbname)
955
956        # Set the column that inline searching will use.
957        self._widget.set_enable_search(True)
958        #self._widget.set_search_column(3)
959
960        targ_data = ((ClipboardListView.LOCAL_DRAG_TARGET,) +
961                     DdTargets.all_targets())
962        self._widget.drag_dest_set(Gtk.DestDefaults.ALL, targ_data,
963                                   Gdk.DragAction.COPY)
964
965        self._widget.connect('drag-data-get', self.object_drag_data_get)
966        self._widget.connect('drag-begin', self.object_drag_begin)
967        self._widget.connect_after('drag-begin', self.object_after_drag_begin)
968        self._widget.connect('drag-data-received',
969                             self.object_drag_data_received)
970        self._widget.connect('drag-end', self.object_drag_end)
971
972        self.register_wrapper_classes()
973
974    def database_changed(self, db):
975        if not db.is_open():
976            return
977        global clipdb
978        clipdb = db
979        # Note: delete event is emitted before the delete, so checking
980        #        if valid on this is useless !
981        db_signals = (
982            'person-update',
983            'person-rebuild',
984            'family-update',
985            'family-rebuild',
986            'source-update',
987            'source-rebuild',
988            'place-update',
989            'place-rebuild',
990            'media-update',
991            'media-rebuild',
992            'event-update',
993            'event-rebuild',
994            'repository-update',
995            'repository-rebuild',
996            'note-rebuild')
997
998        for signal in db_signals:
999            clipdb.connect(signal, self.refresh_objects)
1000
1001        clipdb.connect('person-delete',
1002                       gen_del_obj(self.delete_object, 'person-link'))
1003        clipdb.connect('person-delete',
1004                       gen_del_obj(self.delete_object_ref, 'personref'))
1005        clipdb.connect('person-delete',
1006                       gen_del_obj(self.delete_object_ref, 'childref'))
1007        clipdb.connect('source-delete',
1008                       gen_del_obj(self.delete_object, 'source-link'))
1009        clipdb.connect('source-delete',
1010                       gen_del_obj(self.delete_object_ref, 'srcref'))
1011        clipdb.connect('repository-delete',
1012                       gen_del_obj(self.delete_object, 'repo-link'))
1013        clipdb.connect('event-delete',
1014                       gen_del_obj(self.delete_object, 'pevent'))
1015        clipdb.connect('event-delete',
1016                       gen_del_obj(self.delete_object_ref, 'eventref'))
1017        clipdb.connect('media-delete',
1018                       gen_del_obj(self.delete_object, 'media'))
1019        clipdb.connect('media-delete',
1020                       gen_del_obj(self.delete_object_ref, 'mediaref'))
1021        clipdb.connect('place-delete',
1022                       gen_del_obj(self.delete_object, 'place-link'))
1023        clipdb.connect('note-delete',
1024                       gen_del_obj(self.delete_object, 'note-link'))
1025        # family-delete not needed, cannot be dragged!
1026
1027        self.refresh_objects()
1028
1029    def refresh_objects(self, dummy=None):
1030        model = self._widget.get_model()
1031
1032        if model:
1033            for _ob in model:
1034                if not _ob[1].is_valid():
1035                    model.remove(_ob.iter)
1036                else:
1037                    _ob[1].refresh()
1038                    _ob[4] = _ob[1].get_value()  # Force listview to update
1039
1040    def delete_object(self, handle_list, link_type):
1041        model = self._widget.get_model()
1042
1043        if model:
1044            for _ob in model:
1045                if _ob[0] == link_type:
1046                    data = pickle.loads(_ob[1]._obj)
1047                    if data[2] in handle_list:
1048                        model.remove(_ob.iter)
1049
1050    def delete_object_ref(self, handle_list, link_type):
1051        model = self._widget.get_model()
1052
1053        if model:
1054            for _ob in model:
1055                if _ob[0] == link_type:
1056                    data = _ob[1]._obj.get_reference_handle()
1057                    if data in handle_list:
1058                        model.remove(_ob.iter)
1059
1060    # Method to manage the wrapper classes.
1061
1062    def register_wrapper_classes(self):
1063        self.register_wrapper_class(ClipAddress)
1064        self.register_wrapper_class(ClipLocation)
1065        self.register_wrapper_class(ClipEvent)
1066        self.register_wrapper_class(ClipPlace)
1067        self.register_wrapper_class(ClipEventRef)
1068        self.register_wrapper_class(ClipPlaceRef)
1069        self.register_wrapper_class(ClipRepoRef)
1070        self.register_wrapper_class(ClipFamilyEvent)
1071        self.register_wrapper_class(ClipUrl)
1072        self.register_wrapper_class(ClipAttribute)
1073        self.register_wrapper_class(ClipFamilyAttribute)
1074        self.register_wrapper_class(ClipName)
1075        self.register_wrapper_class(ClipPlaceName)
1076        self.register_wrapper_class(ClipRepositoryLink)
1077        self.register_wrapper_class(ClipMediaObj)
1078        self.register_wrapper_class(ClipMediaRef)
1079        self.register_wrapper_class(ClipSourceLink)
1080        self.register_wrapper_class(ClipCitation)
1081        self.register_wrapper_class(ClipPersonLink)
1082        self.register_wrapper_class(ClipFamilyLink)
1083        self.register_wrapper_class(ClipDropList)
1084        self.register_wrapper_class(ClipDropRawList)
1085        self.register_wrapper_class(ClipDropHandleList)
1086        self.register_wrapper_class(ClipPersonRef)
1087        self.register_wrapper_class(ClipChildRef)
1088        self.register_wrapper_class(ClipText)
1089        self.register_wrapper_class(ClipNote)
1090
1091    def register_wrapper_class(self, wrapper_class):
1092        for drop_target in wrapper_class.DROP_TARGETS:
1093            self._target_type_to_wrapper_class_map[
1094                drop_target.drag_type] = wrapper_class
1095
1096    # Methods for rendering the cells.
1097
1098    def object_pixbuf(self, column, cell, model, node, user_data=None):
1099        _ob = model.get_value(node, 1)
1100        if _ob._dbid is not None and _ob._dbid != self.dbstate.db.get_dbid():
1101            if isinstance(_ob.__class__.UNAVAILABLE_ICON, str):
1102                cell.set_property('icon-name',
1103                                  _ob.__class__.UNAVAILABLE_ICON)
1104            else:
1105                cell.set_property('pixbuf',
1106                                  _ob.__class__.UNAVAILABLE_ICON)
1107        else:
1108            cell.set_property('pixbuf', _ob.__class__.ICON)
1109
1110    def object_type(self, column, cell, model, node, user_data=None):
1111        _ob = model.get_value(node, 1)
1112        cell.set_property('text', _ob.get_type())
1113
1114    def object_title(self, column, cell, model, node, user_data=None):
1115        _ob = model.get_value(node, 1)
1116        cell.set_property('text', _ob.get_title())
1117
1118    def object_value(self, column, cell, model, node, user_data=None):
1119        _ob = model.get_value(node, 1)
1120        cell.set_property('text', _ob.get_value())
1121
1122    def get_dbname(self, column, cell, model, node, user_data=None):
1123        _ob = model.get_value(node, 1)
1124        cell.set_property('text', _ob.get_dbname())
1125
1126    # handlers for the drag and drop events.
1127
1128    def on_object_select_row(self, obj):
1129        tree_selection = self._widget.get_selection()
1130        model, paths = tree_selection.get_selected_rows()
1131        if len(paths) > 1:
1132            targets = [DdTargets.RAW_LIST.target(),
1133                       ClipboardListView.LOCAL_DRAG_TARGET]
1134        else:
1135            targets = [ClipboardListView.LOCAL_DRAG_TARGET]
1136        for path in paths:
1137            node = model.get_iter(path)
1138            if node is not None:
1139                _ob = model.get_value(node, 1)
1140                targets += [target.target()
1141                            for target in _ob.__class__.DROP_TARGETS]
1142
1143        self._widget.enable_model_drag_source(
1144            Gdk.ModifierType.BUTTON1_MASK, targets,
1145            Gdk.DragAction.COPY | Gdk.DragAction.MOVE)
1146
1147    def object_drag_begin(self, widget, drag_context):
1148        """ Handle the beginning of a drag operation. """
1149        pass
1150
1151    def object_after_drag_begin(self, widget, drag_context):
1152        tree_selection = widget.get_selection()
1153        model, paths = tree_selection.get_selected_rows()
1154        if len(paths) == 1:
1155            path = paths[0]
1156            node = model.get_iter(path)
1157            target = model.get_value(node, 0)
1158            if target == "TEXT":
1159                layout = widget.create_pango_layout(model.get_value(node, 4))
1160                layout.set_alignment(Pango.Alignment.CENTER)
1161                width, height = layout.get_pixel_size()
1162                surface = cairo.ImageSurface(cairo.FORMAT_RGB24,
1163                                             width, height)
1164                ctx = cairo.Context(surface)
1165                style_ctx = self._widget.get_style_context()
1166                Gtk.render_background(style_ctx, ctx, 0, 0, width, height)
1167                ctx.save()
1168                Gtk.render_layout(style_ctx, ctx, 0, 0 , layout)
1169                ctx.restore()
1170                Gtk.drag_set_icon_surface(drag_context, surface)
1171            else:
1172                try:
1173                    if map2class(target):
1174                        Gtk.drag_set_icon_pixbuf(drag_context,
1175                                                 map2class(target).ICON, 0, 0)
1176                except:
1177                    Gtk.drag_set_icon_default(drag_context)
1178
1179    def object_drag_end(self, widget, drag_context):
1180        """ Handle the end of a drag operation. """
1181        pass
1182
1183    def object_drag_data_get(self, widget, context, sel_data, info, time):
1184        tree_selection = widget.get_selection()
1185        model, paths = tree_selection.get_selected_rows()
1186        tgs = context.list_targets()
1187        if len(paths) == 1:
1188            path = paths[0]
1189            node = model.get_iter(path)
1190            _ob = model.get_value(node, 1)
1191            if model.get_value(node, 0) == 'TEXT':
1192                sel_data.set_text(_ob._value, -1)
1193            else:
1194                sel_data.set(tgs[0], 8, _ob.pack())
1195        elif len(paths) > 1:
1196            raw_list = []
1197            for path in paths:
1198                node = model.get_iter(path)
1199                _ob = model.get_value(node, 1)
1200                raw_list.append(_ob.pack())
1201            sel_data.set(tgs[0], 8, pickle.dumps(raw_list))
1202
1203    def object_drag_data_received(self, widget, context, x, y, selection, info,
1204                                  time, title=None, value=None, dbid=None,
1205                                  dbname=None):
1206        model = widget.get_model()
1207        sel_data = selection.get_data()
1208        # In Windows time is always zero. Until that is fixed, use the seconds
1209        # of the local time to filter out double drops.
1210        real_time = strftime("%S")
1211
1212        # There is a strange bug that means that if there is a selection
1213        # in the list we get multiple drops of the same object. Luckily
1214        # the time values are the same so we can drop all but the first.
1215        if (real_time == self._previous_drop_time) and (time != -1):
1216            return None
1217
1218        # Find a wrapper class
1219        possible_wrappers = []
1220        if mac():
1221            # context is empty on mac due to a bug, work around this
1222            # Note that this workaround code works fine in linux too as
1223            # we know very well inside of Gramps what sel_data can be, so
1224            # we can anticipate on it, instead of letting the wrapper handle
1225            # it. This is less clean however !
1226            # See http://www.gramps-project.org/bugs/view.php?id=3089 for
1227            # an explaination of why this is required.
1228            dragtype = None
1229            try:
1230                dragtype = pickle.loads(sel_data)[0]
1231            except pickle.UnpicklingError as msg :
1232                # not a pickled object, probably text
1233                if isinstance(sel_data, str):
1234                    dragtype = DdTargets.TEXT.drag_type
1235            if dragtype in self._target_type_to_wrapper_class_map:
1236                possible_wrappers = [dragtype]
1237        else:
1238            tgs = [atm.name() for atm in context.list_targets()]
1239            possible_wrappers = [
1240                target for target in tgs
1241                if target in self._target_type_to_wrapper_class_map]
1242
1243        if not possible_wrappers:
1244            # No wrapper for this class
1245            return None
1246
1247        # Just select the first match.
1248        wrapper_class = self._target_type_to_wrapper_class_map[
1249            str(possible_wrappers[0])]
1250        try:
1251            _ob = wrapper_class(sel_data)
1252            if title:
1253                _ob._title = title
1254            if value:
1255                _ob._value = value
1256            if dbid:
1257                _ob._dbid = dbid
1258            if dbname:
1259                _ob._dbname = dbname
1260
1261            # If this was a ClipPersonLink with a list for a handle, we don't
1262            # want it.  Can happen in People Treeview with attemp to drag last
1263            # name group.
1264            if (isinstance(_ob, ClipHandleWrapper) and
1265                    isinstance(_ob._handle, list)):
1266                return None
1267            # If the wrapper object is a subclass of ClipDropList then
1268            # the drag data was a list of objects and we need to decode
1269            # all of them.
1270            if isinstance(_ob, ClipDropList):
1271                o_list = _ob.get_objects()
1272            else:
1273                o_list = [_ob]
1274            for _ob in o_list:
1275                if _ob.__class__.DRAG_TARGET is None:
1276                    continue
1277                data = [_ob.__class__.DRAG_TARGET.drag_type, _ob, None,
1278                        _ob._type, _ob._value, _ob._dbid, _ob._dbname]
1279                contains = model_contains(model, data)
1280                if(contains and not
1281                   (context.get_actions() & Gdk.DragAction.MOVE)):
1282                    continue
1283                drop_info = widget.get_dest_row_at_pos(x, y)
1284                if drop_info:
1285                    path, position = drop_info
1286                    node = model.get_iter(path)
1287                    if position == Gtk.TreeViewDropPosition.BEFORE or \
1288                       position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE:
1289
1290                        model.insert_before(node, data)
1291                    else:
1292                        model.insert_after(node, data)
1293                elif isinstance(data[1], ClipCitation):
1294                    if data[3]:
1295                        # we have a real citation
1296                        model.append(data)
1297                    #else:
1298                    #   We are in a Source treeview and trying
1299                    #   to copy a source with a shortcut.
1300                    #   Use drag and drop to do that.
1301                else:
1302                    model.append(data)
1303
1304            # FIXME: there is one bug here: if you multi-select and drop
1305            # on self, then it moves the first, and copies the rest.
1306
1307            if context.get_actions() & Gdk.DragAction.MOVE:
1308                context.finish(True, True, time)
1309
1310            # remember time for double drop workaround.
1311            self._previous_drop_time = real_time
1312            return o_list
1313        except EOFError:
1314            return None
1315
1316    # proxy methods to provide access to the real widget functions.
1317
1318    def set_model(self, model=None):
1319        self._widget.set_model(model)
1320        self._widget.get_selection().connect('changed',
1321                                             self.on_object_select_row)
1322        self._widget.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
1323
1324    def get_model(self):
1325        return self._widget.get_model()
1326
1327    def get_selection(self):
1328        return self._widget.get_selection()
1329
1330    def set_search_column(self, col):
1331        return self._widget.set_search_column(col)
1332
1333
1334#-------------------------------------------------------------------------
1335#
1336# ClipboardWindow class
1337#
1338#-------------------------------------------------------------------------
1339class ClipboardWindow(ManagedWindow):
1340    """
1341        The Clipboard provides a temporary area to hold objects that can
1342        be reused accross multiple Person records. The pad provides a window
1343        onto which objects can be dropped and then dragged into new Person
1344        dialogs. The objects are stored as the pickles that are built by the
1345        origininating widget. The objects are only unpickled in order to
1346        provide the text in the display.
1347
1348        No attempt is made to ensure that any references contained within
1349        the pickles are valid. Because the pad extends the life time of drag
1350        and drop objects, it is possible that references that were valid
1351        when an object is copied to the pad are invalid by the time they
1352        are dragged to a new Person. For this reason, using the pad places
1353        a responsibility on all '_drag_data_received' methods to check the
1354        references of objects before attempting to use them.
1355        """
1356
1357    # Class attribute used to hold the content of the Clipboard.
1358    # A class attribute is used so that the content
1359    # it preserved even when the Clipboard window is closed.
1360    # As there is only ever one Clipboard we do not need to
1361    # maintain a list of these.
1362    otree = None
1363
1364    def __init__(self, dbstate, uistate):
1365        """Initialize the ClipboardWindow class, and display the window"""
1366
1367        ManagedWindow.__init__(self, uistate, [], self.__class__)
1368        self.dbstate = dbstate
1369
1370        self.database_changed(self.dbstate.db)
1371        self.dbstate.connect('database-changed', self.database_changed)
1372
1373        self.top = Glade()
1374        self.set_window(self.top.toplevel, None, None, msg=_("Clipboard"))
1375        self.setup_configs('interface.clipboard', 500, 300)
1376
1377        self.clear_all_btn = self.top.get_object("btn_clear_all")
1378        self.clear_btn = self.top.get_object("btn_clear")
1379        objectlist = self.top.get_object('objectlist')
1380        mtv = MultiTreeView(self.dbstate, self.uistate, _("Clipboard"))
1381        scrolledwindow = self.top.get_object('scrolledwindow86')
1382        scrolledwindow.remove(objectlist)
1383        scrolledwindow.add(mtv)
1384        self.object_list = ClipboardListView(self.dbstate, mtv)
1385        self.object_list.get_selection().connect(
1386            'changed', self.set_clear_btn_sensitivity)
1387        self.object_list.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
1388        self.set_clear_btn_sensitivity(sel=self.object_list.get_selection())
1389
1390        if not ClipboardWindow.otree:
1391            ClipboardWindow.otree = ClipboardListModel()
1392
1393        self.set_clear_all_btn_sensitivity(treemodel=ClipboardWindow.otree)
1394        ClipboardWindow.otree.connect('row-deleted',
1395                                      self.set_clear_all_btn_sensitivity)
1396        ClipboardWindow.otree.connect('row-inserted',
1397                                      self.set_clear_all_btn_sensitivity)
1398
1399        self.object_list.set_model(ClipboardWindow.otree)
1400
1401        #Database might have changed, objects might have been removed,
1402        #we need to reevaluate if all data is valid
1403        self.object_list.refresh_objects()
1404
1405        self.top.connect_signals({
1406            "on_close_clipboard" : self.close,
1407            "on_clear_clicked": self.on_clear_clicked,
1408            "on_help_clicked": self.on_help_clicked})
1409
1410        self.clear_all_btn.connect_object('clicked', Gtk.ListStore.clear,
1411                                          ClipboardWindow.otree)
1412        self.db.connect('database-changed',
1413                        lambda x: ClipboardWindow.otree.clear())
1414
1415        self.show()
1416
1417    def build_menu_names(self, obj):
1418        return (_('Clipboard'), None)
1419
1420    def database_changed(self, database):
1421        self.db = database
1422
1423    def set_clear_all_btn_sensitivity(self, treemodel=None,
1424                                      path=None, node=None, user_param1=None):
1425        if treemodel:
1426            self.clear_all_btn.set_sensitive(True)
1427        else:
1428            self.clear_all_btn.set_sensitive(False)
1429
1430    def set_clear_btn_sensitivity(self, sel=None, user_param1=None):
1431        if sel.count_selected_rows() == 0:
1432            self.clear_btn.set_sensitive(False)
1433        else:
1434            self.clear_btn.set_sensitive(True)
1435
1436    def on_help_clicked(self, obj):
1437        """Display the relevant portion of Gramps manual"""
1438        display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC)
1439
1440    def on_clear_clicked(self, obj):
1441        """Deletes the selected object from the object list"""
1442        selection = self.object_list.get_selection()
1443        model, paths = selection.get_selected_rows()
1444        paths.reverse()
1445        for path in paths:
1446            node = model.get_iter(path)
1447            if node:
1448                model.remove(node)
1449
1450
1451#-------------------------------------------------------------------------
1452#
1453# MultiTreeView class
1454#
1455#-------------------------------------------------------------------------
1456class MultiTreeView(Gtk.TreeView):
1457    '''
1458    TreeView that captures mouse events to make drag and drop work properly
1459    '''
1460    def __init__(self, dbstate, uistate, title=None):
1461        self.dbstate = dbstate
1462        self.uistate = uistate
1463        self.title = title if title else _("Clipboard")
1464        Gtk.TreeView.__init__(self)
1465        self.connect('button_press_event', self.on_button_press)
1466        self.connect('button_release_event', self.on_button_release)
1467        self.connect('drag-end', self.on_drag_end)
1468        self.connect('key_press_event', self.key_press_event)
1469        self.defer_select = False
1470
1471    def key_press_event(self, widget, event):
1472        if event.type == Gdk.EventType.KEY_PRESS:
1473            if event.keyval == Gdk.KEY_Delete:
1474                model, paths = self.get_selection().get_selected_rows()
1475                # reverse, to delete from the end
1476                paths.sort(key=lambda x: -x[0])
1477                for path in paths:
1478                    try:
1479                        node = model.get_iter(path)
1480                    except:
1481                        node = None
1482                    if node:
1483                        model.remove(node)
1484                return True
1485
1486    def on_button_press(self, widget, event):
1487        # Here we intercept mouse clicks on selected items so that we can
1488        # drag multiple items without the click selecting only one
1489        target = self.get_path_at_pos(int(event.x), int(event.y))
1490        if is_right_click(event):
1491            selection = widget.get_selection()
1492            store, paths = selection.get_selected_rows()
1493            if not paths:
1494                return
1495            tpath = paths[0] if paths else None
1496            node = store.get_iter(tpath) if tpath else None
1497            _ob = None
1498            if node:
1499                _ob = store.get_value(node, 1)
1500            self.newmenu = Gtk.Menu()
1501            popup = self.newmenu
1502            # ---------------------------
1503            if _ob:
1504                objclass, handle = _ob._objclass, _ob._handle
1505            else:
1506                objclass, handle = None, None
1507            if obj2class(objclass):
1508                if self.dbstate.db.method('has_%s_handle', objclass)(handle):
1509                    menu_item = Gtk.MenuItem(
1510                        label=_("the object|See %s details") %
1511                        glocale.trans_objclass(objclass))
1512                    menu_item.connect(
1513                        "activate",
1514                        lambda widget: self.edit_obj(objclass, handle))
1515                    popup.append(menu_item)
1516                    menu_item.show()
1517                    # ---------------------------
1518                    menu_item = Gtk.MenuItem(
1519                        label=_("the object|Make %s active") %
1520                        glocale.trans_objclass(objclass))
1521                    menu_item.connect(
1522                        "activate", lambda widget:
1523                        self.uistate.set_active(handle, objclass))
1524                    popup.append(menu_item)
1525                    menu_item.show()
1526                # ---------------------------
1527                gids = set()
1528                for path in paths:
1529                    node = store.get_iter(path)
1530                    if node:
1531                        _ob = store.get_value(node, 1)
1532                        if _ob._objclass == objclass:
1533                            my_handle = _ob._handle
1534                            if self.dbstate.db.method('has_%s_handle',
1535                                                      objclass)(my_handle):
1536                                obj = self.dbstate.db.method(
1537                                    'get_%s_from_handle', objclass)(my_handle)
1538                                gids.add(obj.gramps_id)
1539                if gids:
1540                    menu_item = Gtk.MenuItem(
1541                        label=_("the object|Create Filter from %s "
1542                                "selected...") %
1543                        glocale.trans_objclass(objclass))
1544                    menu_item.connect("activate", lambda widget: make_filter(
1545                        self.dbstate, self.uistate,
1546                        objclass, gids, title=self.title))
1547                    popup.append(menu_item)
1548                    menu_item.show()
1549            if popup.get_children():  # Show the popup menu:
1550                popup.popup(None, None, None, None, 3, event.time)
1551            return True
1552        elif (event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS
1553                  and event.button == 1):
1554            model, paths = self.get_selection().get_selected_rows()
1555            for path in paths:
1556                node = model.get_iter(path)
1557                if node is not None:
1558                    _ob = model.get_value(node, 1)
1559                    objclass = _ob._objclass
1560                    handle = _ob._handle
1561                    self.edit_obj(objclass, handle)
1562            return True
1563        # otherwise:
1564        if (target and
1565                event.type == Gdk.EventType.BUTTON_PRESS and
1566                self.get_selection().path_is_selected(target[0]) and
1567                no_match_primary_mask(event.get_state(),
1568                                      Gdk.ModifierType.SHIFT_MASK)):
1569            # disable selection
1570            self.get_selection().set_select_function(
1571                lambda *ignore: False, None)
1572            self.defer_select = target[0]
1573
1574    def on_button_release(self, widget, event):
1575        # re-enable selection
1576        self.get_selection().set_select_function(lambda *ignore: True, None)
1577
1578        target = self.get_path_at_pos(int(event.x), int(event.y))
1579        if (self.defer_select and target and
1580                self.defer_select == target[0] and not
1581                (event.x == 0 and
1582                 event.y == 0)):  # certain drag and drop
1583            self.set_cursor(target[0], target[1], False)
1584
1585        self.defer_select = False
1586
1587    def on_drag_end(self, widget, event):
1588        # re-enable selection
1589        self.get_selection().set_select_function(lambda *ignore: True, None)
1590        self.defer_select = False
1591
1592    def edit_obj(self, objclass, handle):
1593        from .editors import (EditPerson, EditEvent, EditFamily, EditSource,
1594                              EditPlace, EditRepository, EditNote, EditMedia,
1595                              EditCitation)
1596        if obj2class(objclass):  # make sure it is an editable object
1597            if self.dbstate.db.method('has_%s_handle', objclass)(handle):
1598                g_object = self.dbstate.db.method(
1599                    'get_%s_from_handle', objclass)(handle)
1600                try:
1601                    locals()['Edit' + objclass](
1602                        self.dbstate, self.uistate, [], g_object)
1603                except WindowActiveError:
1604                    pass
1605
1606
1607def short(val, size=60):
1608    if len(val) > size:
1609        return "%s..." % val[0:size]
1610    return val
1611
1612
1613def place_title(db, event):
1614    return place_displayer.display_event(db, event)
1615
1616
1617def gen_del_obj(func, t):
1618    return lambda l : func(l, t)
1619
1620
1621#-------------------------------------------------------------------------
1622#
1623#
1624#
1625#-------------------------------------------------------------------------
1626def Clipboard(database, person, callback, parent=None):
1627    ClipboardWindow(database, parent)
1628