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