William Bowling is sharing code with you
Bitbucket is a code hosting site. Unlimited public and private repositories. Free for small teams.
Don't show this again1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 | /*
* Adium is the legal property of its developers, whose names are listed in the copyright file included
* with this source distribution.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation; either version 2 of the License,
* or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program; if not,
* write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#import "AIListWindowController.h"
#import "AISCLViewPlugin.h"
#import <Adium/AIListOutlineView.h>
#import <Adium/AIChatControllerProtocol.h>
#import <Adium/AIAccountControllerProtocol.h>
#import <Adium/AIInterfaceControllerProtocol.h>
#import <Adium/AIDockControllerProtocol.h>
#import <AIUtilities/AIWindowAdditions.h>
#import <AIUtilities/AIFunctions.h>
#import <AIUtilities/AIWindowControllerAdditions.h>
#import <AIUtilities/AIApplicationAdditions.h>
#import <AIUtilities/AIImageAdditions.h>
#import <AIUtilities/AIOutlineViewAdditions.h>
#import <Adium/AIListBookmark.h>
#import <Adium/AIListContact.h>
#import <Adium/AIListGroup.h>
#import <Adium/AIListObject.h>
#import <Adium/AIProxyListObject.h>
#import <Adium/AIUserIcons.h>
#import <AIUtilities/AIDockingWindow.h>
#import <AIUtilities/AIEventAdditions.h>
#import <Adium/AIContactList.h>
#import <Adium/AIContactHidingController.h>
#import "AISearchFieldCell.h"
#define KEY_HIDE_CONTACT_LIST_GROUPS @"Hide Contact List Groups"
#define SLIDE_ALLOWED_RECT_EDGE_MASK (AIMinXEdgeMask | AIMaxXEdgeMask) /* Screen edges on which sliding is allowde */
#define DOCK_HIDING_MOUSE_POLL_INTERVAL 0.1 /* Interval at which to check the mouse position for sliding */
#define WINDOW_SLIDING_DELAY 0.2 /* Time after the mouse is in the right place before the window slides on screen */
#define WINDOW_ALIGNMENT_TOLERANCE 2.0f /* Threshold distance far the window from an edge to be considered on it */
#define MOUSE_EDGE_SLIDE_ON_DISTANCE 1.1f /* ??? */
#define WINDOW_SLIDING_MOUSE_DISTANCE_TOLERANCE 3.0f /* Distance the mouse must be from the window's frame to be considered outside it */
#define SNAP_DISTANCE 15.0 /* Distance beween one window's edge and another's at which they should snap together */
@interface AIListWindowController ()
- (id)initWithContactList:(AIListObject<AIContainingObject> *)contactList;
+ (NSString *)nibName;
+ (void)updateScreenSlideBoundaryRect:(id)sender;
- (BOOL)shouldSlideWindowOffScreen_mousePositionStrategy;
- (void)slideWindowIfNeeded:(id)sender;
- (BOOL)shouldSlideWindowOnScreen_mousePositionStrategy;
- (void)delayWindowSlidingForInterval:(NSTimeInterval)inDelayTime;
- (void)showFilterBarWithAnimation:(BOOL)flag;
- (void)hideFilterBarWithAnimation:(BOOL)flag;
- (void)animateFilterBarWithDuration:(CGFloat)duration;
@end
@implementation AIListWindowController
@synthesize windowAnimation, filterBarAnimation;
static NSMutableDictionary *screenSlideBoundaryRectDictionary = nil;
+ (void)initialize
{
if ([self isEqual:[AIListWindowController class]]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateScreenSlideBoundaryRect:)
name:NSApplicationDidChangeScreenParametersNotification
object:nil];
[self updateScreenSlideBoundaryRect:nil];
}
}
+ (AIListWindowController *)listWindowControllerForContactList:(AIListObject<AIContainingObject> *)contactList
{
return [[[self alloc] initWithContactList:contactList] autorelease];
}
- (id)initWithContactList:(AIListObject<AIContainingObject> *)contactList
{
if ((self = [self initWithWindowNibName:[[self class] nibName]])) {
preventHiding = NO;
previousAlpha = 0;
[NSBundle loadNibNamed:@"Filter Bar" owner:self];
[self setContactList:contactList];
}
return self;
}
- (AIListObject<AIContainingObject> *)contactList
{
return (contactListRoot ? contactListRoot : [contactListController contactList]);
}
- (AIListController *) listController
{
return contactListController;
}
- (AIListOutlineView *)contactListView
{
return contactListView;
}
- (void)setContactList:(AIListObject<AIContainingObject> *)inContactList
{
if (inContactList != contactListRoot) {
[contactListRoot release];
contactListRoot = [inContactList retain];
}
}
//Our window nib name
+ (NSString *)nibName
{
return @"";
}
- (Class)listControllerClass
{
return [AIListController class];
}
- (void)dealloc
{
[searchField setDelegate:nil];
[filterBarAnimation stopAnimation];
[filterBarAnimation setDelegate:nil];
self.filterBarAnimation = nil;
[filterBarPreviouslySelected release];
[[NSNotificationCenter defaultCenter] removeObserver:self];
[windowAnimation stopAnimation];
[windowAnimation setDelegate:nil];
self.windowAnimation = nil;
[contactListController close];
[windowLastScreen release];
[super dealloc];
}
- (NSString *)adiumFrameAutosaveName
{
AILogWithSignature(@"My autosave name is %@",[NSString stringWithFormat:@"Contact List:%@", [[self contactList] contentsBasedIdentifier]]);
return [NSString stringWithFormat:@"Contact List:%@", [[self contactList] contentsBasedIdentifier]];
}
//Setup the window after it has loaded
- (void)windowDidLoad
{
contactListController = [[[self listControllerClass] alloc] initWithContactList:[self contactList]
inOutlineView:contactListView
inScrollView:scrollView_contactList
delegate:self];
//super's windowDidLoad will restore our location, which is based upon the contactListRoot
[super windowDidLoad];
//Exclude this window from the window menu (since we add it manually)
[[self window] setExcludedFromWindowsMenu:YES];
[[self window] useOptimizedDrawing:YES];
minWindowSize = [[self window] minSize];
[contactListController setMinWindowSize:minWindowSize];
[[self window] setTitle:AILocalizedString(@"Contacts","Contact List window title")];
//Watch for resolution and screen configuration changes
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(screenParametersChanged:)
name:NSApplicationDidChangeScreenParametersNotification
object:nil];
// Filter bar
filterBarExpandedGroups = NO;
filterBarIsVisible = NO;
filterBarShownAutomatically = NO;
self.filterBarAnimation = nil;
filterBarPreviouslySelected = nil;
[searchField setDelegate:self];
//Show the contact list initially even if it is at a screen edge and supposed to slide out of view
[self delayWindowSlidingForInterval:5];
id<AIPreferenceController> preferenceController = adium.preferenceController;
//Observe preference changes
[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST];
[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST_DISPLAY];
[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
//Preference code below assumes layout is done before theme.
[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_LAYOUT];
[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_THEME];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidUnhide:)
name:NSApplicationDidUnhideNotification
object:nil];
//Substitute an otherwise identical copy of the search field for one of our class. We don't want to globally pose as class; we just want it here.
[NSKeyedArchiver setClassName:@"AISearchFieldCell" forClass:[NSSearchFieldCell class]];
[searchField setCell:[NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:[searchField cell]]]];
[NSKeyedArchiver setClassName:@"NSSearchFieldCell" forClass:[NSSearchFieldCell class]];
/* Get rid of the "x" button in the search field that would clear the search.
* It conflicts with the other "x" button that hides the entire bar, and clearing a few characters is probably not necessary.
*/
[[searchField cell] setCancelButtonCell:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowDidResignMain:)
name:NSWindowDidResignMainNotification
object:[self window]];
//Save our frame immediately for sliding purposes
[self setSavedFrame:[[self window] frame]];
}
//Close the contact list window
- (void)windowWillClose:(NSNotification *)notification
{
if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
//Hide the window while it's still off-screen
[[self window] setAlphaValue:0.0];
AILogWithSignature(@"Setting to alpha 0 while the window is offscreen");
//Then move it back on screen so that we'll save the proper position in -[AIWindowController windowWillClose:]
[self slideWindowOnScreenWithAnimation:NO];
}
// When closing the contact list while a search is in progress, reset visibility first.
if (![[searchField stringValue] isEqualToString:@""]) {
[searchField setStringValue:@""];
[self filterContacts:searchField];
}
[super windowWillClose:notification];
//Invalidate the dock-like hiding timer
[slideWindowIfNeededTimer invalidate]; [slideWindowIfNeededTimer release];
//Stop observing
[adium.preferenceController unregisterPreferenceObserver:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
//Tell the interface to unload our window
NSNotificationCenter *adiumNotificationCenter = [NSNotificationCenter defaultCenter];
[adiumNotificationCenter postNotificationName:Interface_ContactListDidResignMain object:self];
[adiumNotificationCenter postNotificationName:Interface_ContactListDidClose object:self];
}
NSInteger levelForAIWindowLevel(AIWindowLevel windowLevel)
{
NSInteger level;
switch (windowLevel) {
case AINormalWindowLevel: level = NSNormalWindowLevel; break;
case AIFloatingWindowLevel: level = NSFloatingWindowLevel; break;
case AIDesktopWindowLevel: level = kCGBackstopMenuLevel; break;
default: level = NSNormalWindowLevel; break;
}
return level;
}
- (void)setWindowLevel:(NSInteger)level
{
AILogWithSignature(@"Setting to %i", level);
[[self window] setLevel:level];
}
//Preferences have changed
- (void)preferencesChangedForGroup:(NSString *)group
key:(NSString *)key
object:(AIListObject *)object
preferenceDict:(NSDictionary *)prefDict
firstTime:(BOOL)firstTime
{
BOOL shouldRevealWindowAndDelaySliding = NO;
// Make sure we're not getting an object-specific update.
if (object != nil)
return;
if ([group isEqualToString:PREF_GROUP_CONTACT_LIST]) {
windowLevel = [[prefDict objectForKey:KEY_CL_WINDOW_LEVEL] integerValue];
[self setWindowLevel:levelForAIWindowLevel(windowLevel)];
listHasShadow = [[prefDict objectForKey:KEY_CL_WINDOW_HAS_SHADOW] boolValue];
[[self window] setHasShadow:listHasShadow];
windowHidingStyle = [[prefDict objectForKey:KEY_CL_WINDOW_HIDING_STYLE] integerValue];
slideOnlyInBackground = [[prefDict objectForKey:KEY_CL_SLIDE_ONLY_IN_BACKGROUND] boolValue];
[[self window] setHidesOnDeactivate:(windowHidingStyle == AIContactListWindowHidingStyleBackground)];
showOnAllSpaces = [[prefDict objectForKey:KEY_CL_ALL_SPACES] boolValue];
[[self window] setCollectionBehavior:showOnAllSpaces ? NSWindowCollectionBehaviorCanJoinAllSpaces : NSWindowCollectionBehaviorDefault];
if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
if (!slideWindowIfNeededTimer) {
slideWindowIfNeededTimer = [[NSTimer scheduledTimerWithTimeInterval:DOCK_HIDING_MOUSE_POLL_INTERVAL
target:self
selector:@selector(slideWindowIfNeeded:)
userInfo:nil
repeats:YES] retain];
}
} else if (slideWindowIfNeededTimer) {
[slideWindowIfNeededTimer invalidate];
[slideWindowIfNeededTimer release]; slideWindowIfNeededTimer = nil;
}
[contactListController setShowTooltips:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS] boolValue]];
[contactListController setShowTooltipsInBackground:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS_IN_BACKGROUND] boolValue]];
}
//Auto-Resizing
if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
AIContactListWindowStyle windowStyle = [[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_STYLE] integerValue];
BOOL autoResizeHorizontally = [[prefDict objectForKey:KEY_LIST_LAYOUT_HORIZONTAL_AUTOSIZE] boolValue];
BOOL autoResizeVertically = YES;
NSInteger forcedWindowWidth, maxWindowWidth;
//Determine how to handle vertical autosizing. AIAppearancePreferences must match this behavior for this to make sense.
switch (windowStyle) {
case AIContactListWindowStyleStandard:
case AIContactListWindowStyleBorderless:
case AIContactListWindowStyleGroupChat:
//Standard and borderless don't have to vertically autosize, but they might
autoResizeVertically = [[prefDict objectForKey:KEY_LIST_LAYOUT_VERTICAL_AUTOSIZE] boolValue];
break;
case AIContactListWindowStyleGroupBubbles:
case AIContactListWindowStyleContactBubbles:
case AIContactListWindowStyleContactBubbles_Fitted:
//The bubbles styles don't show a window; force them to autosize by leaving autoResizeVertically == YES
break;
}
if (autoResizeHorizontally) {
//If autosizing, KEY_LIST_LAYOUT_HORIZONTAL_WIDTH determines the maximum width; no forced width.
maxWindowWidth = [[prefDict objectForKey:KEY_LIST_LAYOUT_HORIZONTAL_WIDTH] integerValue];
forcedWindowWidth = -1;
} else {
if (windowStyle == AIContactListWindowStyleStandard/* || windowStyle == AIContactListWindowStyleBorderless*/) {
//In the non-transparent non-autosizing modes, KEY_LIST_LAYOUT_HORIZONTAL_WIDTH has no meaning
maxWindowWidth = 10000;
forcedWindowWidth = -1;
} else {
//In the transparent non-autosizing modes, KEY_LIST_LAYOUT_HORIZONTAL_WIDTH determines the width of the window
forcedWindowWidth = [[prefDict objectForKey:KEY_LIST_LAYOUT_HORIZONTAL_WIDTH] integerValue];
maxWindowWidth = forcedWindowWidth;
}
}
//Show the resize indicator if either or both of the autoresizing options is NO
[[self window] setShowsResizeIndicator:!(autoResizeVertically && autoResizeHorizontally)];
/*
Reset the minimum and maximum sizes in case [contactListController contactListDesiredSizeChanged]; doesn't cause a sizing change
(and therefore the min and max sizes aren't set there).
*/
NSSize thisMinimumSize = minWindowSize;
NSSize thisMaximumSize = NSMakeSize(maxWindowWidth, 10000);
NSRect currentFrame = [[self window] frame];
if (forcedWindowWidth != -1) {
/*
If we have a forced width but we are doing no autoresizing, set our frame now so we don't have to be doing checks every time
contactListDesiredSizeChanged is called.
*/
if (!(autoResizeVertically || autoResizeHorizontally)) {
thisMinimumSize.width = forcedWindowWidth;
[[self window] setFrame:NSMakeRect(currentFrame.origin.x,currentFrame.origin.y,forcedWindowWidth,currentFrame.size.height)
display:YES
animate:NO];
}
}
//If vertically resizing, make the minimum and maximum heights the current height
if (autoResizeVertically) {
thisMinimumSize.height = currentFrame.size.height;
thisMaximumSize.height = currentFrame.size.height;
}
//If horizontally resizing, make the minimum and maximum widths the current width
if (autoResizeHorizontally) {
thisMinimumSize.width = currentFrame.size.width;
thisMaximumSize.width = currentFrame.size.width;
}
/* For a standard window, inform the contact list that, if asked, it wants to be 175 pixels or more.
* A maximum width less than this can make the list autosize smaller, but if it has its druthers it'll be a sane
* size.
*/
[contactListView setMinimumDesiredWidth:((windowStyle == AIContactListWindowStyleStandard) ? 175 : 0)];
[[self window] setMinSize:thisMinimumSize];
[[self window] setMaxSize:thisMaximumSize];
contactListController.autoResizeHorizontally = autoResizeHorizontally;
contactListController.autoResizeVertically = autoResizeVertically;
[contactListController setForcedWindowWidth:forcedWindowWidth];
[contactListController setMaxWindowWidth:maxWindowWidth];
[contactListController contactListDesiredSizeChanged];
if (!firstTime) {
shouldRevealWindowAndDelaySliding = YES;
}
}
//Window opacity
if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
CGFloat opacity = [[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_OPACITY] doubleValue];
[contactListController setBackgroundOpacity:opacity];
/*
* If we're using fitted bubbles, we want the default behavior of the winodw, which is to respond to clicks on opaque areas
* and ignore clicks on transparent areas. If we're using any other style, we never want to ignore clicks.
*/
BOOL forceWindowToCatchMouseEvents = ([[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_STYLE] integerValue] != AIContactListWindowStyleContactBubbles_Fitted);
if (forceWindowToCatchMouseEvents)
[[self window] setIgnoresMouseEvents:NO];
if (!firstTime) {
shouldRevealWindowAndDelaySliding = YES;
}
}
if ([group isEqualToString:PREF_GROUP_CONTACT_LIST_DISPLAY]) {
[contactListController setUseContactListGroups:![[prefDict objectForKey:KEY_HIDE_CONTACT_LIST_GROUPS] boolValue]];
}
//Layout and Theme ------------
BOOL groupLayout = ([group isEqualToString:PREF_GROUP_LIST_LAYOUT]);
BOOL groupTheme = ([group isEqualToString:PREF_GROUP_LIST_THEME]);
if (groupLayout || (groupTheme && !firstTime)) { /* We don't want to execute this code twice when initializing */
NSDictionary *layoutDict = [adium.preferenceController preferencesForGroup:PREF_GROUP_LIST_LAYOUT];
NSDictionary *themeDict = [adium.preferenceController preferencesForGroup:PREF_GROUP_LIST_THEME];
//Layout only
if (groupLayout) {
NSInteger iconSize = [[layoutDict objectForKey:KEY_LIST_LAYOUT_USER_ICON_SIZE] integerValue];
[AIUserIcons setListUserIconSize:NSMakeSize(iconSize,iconSize)];
}
//Theme only
if (groupTheme || firstTime) {
NSString *imagePath = [themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_PATH];
//Background Image
if (imagePath && [imagePath length] && [[themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_ENABLED] boolValue]) {
[contactListView setBackgroundImage:[[[NSImage alloc] initWithContentsOfFile:imagePath] autorelease]];
} else {
[contactListView setBackgroundImage:nil];
}
}
EXTENDED_STATUS_STYLE statusStyle = [[layoutDict objectForKey:KEY_LIST_LAYOUT_EXTENDED_STATUS_STYLE] integerValue];
EXTENDED_STATUS_POSITION statusPosition = [[layoutDict objectForKey:KEY_LIST_LAYOUT_EXTENDED_STATUS_POSITION] integerValue];
contactListController.autoResizeHorizontallyWithIdleTime =
((statusStyle == IDLE_ONLY || statusStyle == IDLE_AND_STATUS) &&
(statusPosition == EXTENDED_STATUS_POSITION_BESIDE_NAME || statusPosition == EXTENDED_STATUS_POSITION_BOTH));
[contactListController contactListDesiredSizeChanged];
//Both layout and theme
[contactListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
if (!firstTime) {
shouldRevealWindowAndDelaySliding = YES;
}
}
if (shouldRevealWindowAndDelaySliding) {
[self delayWindowSlidingForInterval:2];
[self slideWindowOnScreenWithAnimation:NO];
} else {
//Do a slide immediately if needed (to display as per our new preferneces)
[self slideWindowIfNeeded:nil];
}
}
- (IBAction)performDefaultActionOnSelectedObject:(AIListObject *)selectedObject sender:(NSOutlineView *)sender
{
if ([selectedObject isKindOfClass:[AIListGroup class]]) {
//Expand or collapse the group
for (AIProxyListObject *proxyObject in selectedObject.proxyObjects) {
if ([sender isItemExpanded:proxyObject]) {
[sender collapseItem:proxyObject];
} else {
[sender expandItem:proxyObject];
}
}
} else if ([selectedObject isMemberOfClass:[AIListBookmark class]]) {
//Hide any tooltip the contactListController is currently showing
[contactListController hideTooltip];
[(AIListBookmark *)selectedObject openChat];
} else if ([selectedObject isKindOfClass:[AIListContact class]]) {
//Hide any tooltip the contactListController is currently showing
[contactListController hideTooltip];
//Open a new message with the contact
[adium.interfaceController setActiveChat:[adium.chatController openChatWithContact:(AIListContact *)selectedObject
onPreferredAccount:YES]];
}
}
- (BOOL) canCustomizeToolbar
{
return NO;
}
//Interface Container --------------------------------------------------------------------------------------------------
#pragma mark Interface Container
//Close this container
- (void)close:(id)sender
{
//In response to windowShouldClose, the interface controller releases us. At that point, no one would be retaining
//this instance of AIContactListWindowController, and we would be deallocated. The call to [self window] will
//crash if we are deallocated. A dirty, but functional fix is to temporarily retain ourself here.
[self retain];
if ([self windowShouldClose:nil]) {
[[self window] close];
}
[self release];
}
- (void)makeActive:(id)sender
{
[[self window] makeKeyAndOrderFront:self];
}
//Contact list brought to front
- (void)windowDidBecomeKey:(NSNotification *)notification
{
[[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactListDidBecomeMain object:self];
}
//Contact list sent back
- (void)windowDidResignKey:(NSNotification *)notification
{
[[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactListDidResignMain object:self];
}
- (void)showWindowInFrontIfAllowed:(BOOL)inFront
{
//Always show for three seconds at least if we're told to show
[self delayWindowSlidingForInterval:3];
//Call super to actually do the showing
[super showWindowInFrontIfAllowed:inFront];
NSWindow *window = [self window];
if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
[self slideWindowOnScreenWithAnimation:NO];
}
windowSlidOffScreenEdgeMask = AINoEdges;
currentScreen = [window screen];
currentScreenFrame = [currentScreen frame];
if ([[NSScreen screens] count] &&
(currentScreen == [[NSScreen screens] objectAtIndex:0])) {
currentScreenFrame.size.height -= [[NSApp mainMenu] menuBarHeight];
}
//Ensure the window is displaying at the proper level and exposé setting
[self setWindowLevel:levelForAIWindowLevel(windowLevel)];
}
- (void)setSavedFrame:(NSRect)frame
{
oldFrame = frame;
}
- (NSRect)savedFrame
{
return oldFrame;
}
// Auto-resizing support ------------------------------------------------------------------------------------------------
#pragma mark Auto-resizing support
- (void)respondToScreenParametersChanged:(NSNotification *)notification
{
NSWindow *window = [self window];
NSScreen *windowScreen = [window screen];
if (!windowScreen) {
if ([[NSScreen screens] containsObject:windowLastScreen]) {
windowScreen = windowLastScreen;
} else {
[windowLastScreen release]; windowLastScreen = nil;
windowScreen = [NSScreen mainScreen];
}
}
NSRect newScreenFrame = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowScreen]] rectValue];
if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
NSRect newWindowFrame = AIRectByAligningRect_edge_toRect_edge_([window frame], [self windowSlidOffScreenEdgeMask],
newScreenFrame, [self windowSlidOffScreenEdgeMask]);
[[self window] setFrame:newWindowFrame display:NO];
[self delayWindowSlidingForInterval:2];
[self slideWindowOnScreenWithAnimation:NO];
}
[contactListController contactListDesiredSizeChanged];
currentScreen = [window screen];
currentScreenFrame = newScreenFrame;
[self setSavedFrame:[window frame]];
}
- (void)screenParametersChanged:(NSNotification *)notification
{
/* Wait until the next run loop so the class method has definitely updated our screen sliding borders. */
[self performSelector:@selector(respondToScreenParametersChanged:)
withObject:notification
afterDelay:0];
}
// Printing
#pragma mark Printing
- (void)adiumPrint:(id)sender
{
[contactListView print:sender];
}
// Dock-like hiding -----------------------------------------------------------------------------------------------------
#pragma mark Dock-like hiding
+ (void)updateScreenSlideBoundaryRect:(id)sender
{
NSArray *screens = [NSScreen screens];
NSInteger numScreens = [screens count];
[screenSlideBoundaryRectDictionary release];
screenSlideBoundaryRectDictionary = [[NSMutableDictionary alloc] initWithCapacity:numScreens];
if (numScreens > 0) {
//The menubar screen is a special case - the menubar is not a part of the rect we're interested in
NSScreen *menubarScreen = [screens objectAtIndex:0];
NSRect screenSlideBoundaryRect;
screenSlideBoundaryRect = [menubarScreen frame];
screenSlideBoundaryRect.size.height = NSMaxY([menubarScreen visibleFrame]) - NSMinY([menubarScreen frame]);
[screenSlideBoundaryRectDictionary setObject:[NSValue valueWithRect:screenSlideBoundaryRect]
forKey:[NSValue valueWithNonretainedObject:menubarScreen]];
for (NSInteger i = 1; i < numScreens; i++) {
NSScreen *screen = [screens objectAtIndex:i];
[screenSlideBoundaryRectDictionary setObject:[NSValue valueWithRect:[screen frame]]
forKey:[NSValue valueWithNonretainedObject:screen]];
}
}
}
/*!
* @brief Adium unhid
*
* If the contact list is open but not visible when we unhide, we should always display it; it should not, however, steal focus.
*/
- (void)applicationDidUnhide:(NSNotification *)notification
{
if (![[self window] isVisible]) {
[self showWindowInFrontIfAllowed:NO];
}
}
- (BOOL)windowShouldHideOnDeactivate
{
return (windowHidingStyle == AIContactListWindowHidingStyleBackground);
}
/*!
* @brief Called on a delay by -[self slideWindowIfNeeded:]
*
* This is a separate function so that the call to it may be canceled if the mouse doesn't
* remain in position long enough.
*/
- (void)slideWindowOnScreenAfterDelay
{
waitingToSlideOnScreen = NO;
//If we're hiding the window (generally) but now sliding it on screen, make sure it's on top
if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
[self setWindowLevel:NSFloatingWindowLevel];
[[self window] setCollectionBehavior:showOnAllSpaces ? NSWindowCollectionBehaviorCanJoinAllSpaces : NSWindowCollectionBehaviorDefault];
overrodeWindowLevel = YES;
}
[self slideWindowOnScreen];
}
/*!
* @brief Check what behavior the window should perform and initiate it
*
* Called regularly by a repeating timer to check mouse position against window position.
*/
- (void)slideWindowIfNeeded:(id)sender
{
if ([self shouldSlideWindowOnScreen]) {
if (!waitingToSlideOnScreen) {
[self performSelector:@selector(slideWindowOnScreenAfterDelay)
withObject:nil
afterDelay:WINDOW_SLIDING_DELAY];
waitingToSlideOnScreen = YES;
}
} else {
if (waitingToSlideOnScreen) {
/* If we were waiting to slide on screen but the mouse moved out of position too soon,
* cancel the selector which would slide us on screen.
*/
waitingToSlideOnScreen = NO;
[[self class] cancelPreviousPerformRequestsWithTarget:self
selector:@selector(slideWindowOnScreenAfterDelay)
object:nil];
}
if ([self shouldSlideWindowOffScreen]) {
AIRectEdgeMask adjacentEdges = [self slidableEdgesAdjacentToWindow];
if (adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask)) {
[self slideWindowOffScreenEdges:(adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask))];
} else {
[self slideWindowOffScreenEdges:adjacentEdges];
}
/* If we're hiding the window (generally) but now sliding it off screen, set it to kCGBackstopMenuLevel and don't
* let it participate in expose.
*/
if (overrodeWindowLevel &&
windowHidingStyle == AIContactListWindowHidingStyleSliding) {
[self setWindowLevel:kCGBackstopMenuLevel];
[[self window] setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
overrodeWindowLevel = YES;
}
} else if (overrodeWindowLevel &&
([self slidableEdgesAdjacentToWindow] == AINoEdges) &&
([self windowSlidOffScreenEdgeMask] == AINoEdges)) {
/* If the window level was overridden at some point and now we:
* 1. Are on screen AND
* 2. No longer have any edges eligible for sliding
* we should restore our window level.
*/
[self setWindowLevel:levelForAIWindowLevel(windowLevel)];
[[self window] setCollectionBehavior:showOnAllSpaces ? NSWindowCollectionBehaviorCanJoinAllSpaces : NSWindowCollectionBehaviorDefault];
overrodeWindowLevel = NO;
}
}
}
- (BOOL)shouldSlideWindowOnScreen
{
BOOL shouldSlide = NO;
if (([self windowSlidOffScreenEdgeMask] != AINoEdges) &&
![NSApp isHidden]) {
if (slideOnlyInBackground && [NSApp isActive]) {
//We only slide while in the background, and the app is not in the background. Slide on screen.
shouldSlide = YES;
} else if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
//Slide on screen if the mouse position indicates we should
shouldSlide = [self shouldSlideWindowOnScreen_mousePositionStrategy];
} else {
//It's slid off-screen... and it's not supposed to be sliding at all. Slide back on screen!
shouldSlide = YES;
}
}
return shouldSlide;
}
- (BOOL)shouldSlideWindowOffScreen
{
BOOL shouldSlide = NO;
if ((windowHidingStyle == AIContactListWindowHidingStyleSliding) &&
!preventHiding &&
([self windowSlidOffScreenEdgeMask] == AINoEdges) &&
(!(slideOnlyInBackground && [NSApp isActive]))) {
shouldSlide = [self shouldSlideWindowOffScreen_mousePositionStrategy];
}
return shouldSlide;
}
// slide off screen if the window is aligned to a screen edge and the mouse is not in the strip of screen
// you'd get by translating the window along the screen edge. This is the dock's behavior.
- (BOOL)shouldSlideWindowOffScreen_mousePositionStrategy
{
BOOL shouldSlideOffScreen = NO;
NSWindow *window = [self window];
NSRect windowFrame = [window frame];
NSPoint mouseLocation = [NSEvent mouseLocation];
AIRectEdgeMask slidableEdgesAdjacentToWindow = [self slidableEdgesAdjacentToWindow];
NSRectEdge screenEdge;
for (screenEdge = 0; screenEdge < 4; screenEdge++) {
if (slidableEdgesAdjacentToWindow & (1 << screenEdge)) {
CGFloat distanceMouseOutsideWindow = AISignedExteriorDistanceRect_edge_toPoint_(windowFrame, AIOppositeRectEdge_(screenEdge), mouseLocation);
if (distanceMouseOutsideWindow > WINDOW_SLIDING_MOUSE_DISTANCE_TOLERANCE)
shouldSlideOffScreen = YES;
}
}
/* Don't allow the window to slide off if the user is dragging
* This method is hacky and does not completely work. is there a way to detect if the mouse is down?
*/
NSEventType currentEventType = [[NSApp currentEvent] type];
if (currentEventType == NSLeftMouseDragged ||
currentEventType == NSRightMouseDragged ||
currentEventType == NSOtherMouseDragged ||
currentEventType == NSPeriodic) {
shouldSlideOffScreen = NO;
}
return shouldSlideOffScreen;
}
// note: may be inaccurate when mouse is up against an edge
- (NSScreen *)screenForPoint:(NSPoint)point
{
for (NSScreen *pointScreen in [NSScreen screens]) {
if (NSPointInRect(point, NSInsetRect([pointScreen frame], -1, -1)))
return pointScreen;
}
return nil;
}
- (NSRect)squareRectWithCenter:(NSPoint)point sideLength:(CGFloat)sideLength
{
return NSMakeRect(point.x - sideLength*0.5f, point.y - sideLength*0.5f, sideLength, sideLength);
}
- (BOOL)pointIsInScreenCorner:(NSPoint)point
{
BOOL inCorner = NO;
NSScreen *menubarScreen = [[NSScreen screens] objectAtIndex:0];
CGFloat menubarHeight = NSMaxY([menubarScreen frame]) - NSMaxY([menubarScreen visibleFrame]); // breaks if the dock is at the top of the screen (i.e. if the user is insane)
NSRect screenFrame = [[self screenForPoint:point] frame];
NSPoint lowerLeft = screenFrame.origin;
NSPoint upperRight = NSMakePoint(NSMaxX(screenFrame), NSMaxY(screenFrame));
NSPoint lowerRight = NSMakePoint(upperRight.x, lowerLeft.y);
NSPoint upperLeft = NSMakePoint(lowerLeft.x, upperRight.y);
CGFloat sideLength = menubarHeight * 2.0f;
inCorner = (NSPointInRect(point, [self squareRectWithCenter:lowerLeft sideLength:sideLength])
|| NSPointInRect(point, [self squareRectWithCenter:lowerRight sideLength:sideLength])
|| NSPointInRect(point, [self squareRectWithCenter:upperLeft sideLength:sideLength])
|| NSPointInRect(point, [self squareRectWithCenter:upperRight sideLength:sideLength]));
return inCorner;
}
/*!
* @brief Should the window be slid on screen given the mouse's position?
*
* This method will never return YES of the cl is slid into a corner, which shouldn't happen, or if the mouse is in a corner.
*
* @result YES if the mouse is against all edges of the screen where we previously slid the window and not in a corner.
*/
- (BOOL)shouldSlideWindowOnScreen_mousePositionStrategy
{
if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
NSPoint mouseLocation = [NSEvent mouseLocation];
//Initially, assume the mouse is not in an appropriate position
BOOL mouseNearSlideOffEdges = NO;
NSRectEdge screenEdge;
NSRect screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowLastScreen]] rectValue];
/* Only look at the screen in which the mouse currently resides.
* The mouse may be in no screen if it is over the menu bar.
*/
if (NSPointInRect(mouseLocation, screenSlideBoundaryRect)) {
//Check each edge
for (screenEdge = 0; screenEdge < 4; screenEdge++) {
//But we only care about an edge off of which the window has slid
if (windowSlidOffScreenEdgeMask & (1 << screenEdge)) {
CGFloat mouseOutsideSlideBoundaryRectDistance = AISignedExteriorDistanceRect_edge_toPoint_(screenSlideBoundaryRect,
screenEdge,
mouseLocation);
//The mouse must be within MOUSE_EDGE_SLIDE_ON_DISTANCE of every slid-off edge to bring the window back on-screen
if(mouseOutsideSlideBoundaryRectDistance < -MOUSE_EDGE_SLIDE_ON_DISTANCE) {
mouseNearSlideOffEdges = NO;
break;
} else {
mouseNearSlideOffEdges = YES;
}
}
}
}
return mouseNearSlideOffEdges && ![self pointIsInScreenCorner:mouseLocation];
} else {
return NO;
}
}
#pragma mark Dock-like hiding
- (NSScreen *)windowLastScreen
{
return windowLastScreen;
}
- (BOOL)animationShouldStart:(NSAnimation *)animation
{
if(![animation isEqual:windowAnimation])
return YES;
//Whenever an animation starts, we should be using the normal shadow setting
[[self window] setHasShadow:listHasShadow];
//Don't let docking interfere with the animation
if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
[(id)[self window] setDockingEnabled:NO];
if (windowSlidOffScreenEdgeMask == AINoEdges) {
[[self window] setAlphaValue:previousAlpha];
AILogWithSignature(@"Set window to previous alpha of %f", previousAlpha);
}
return YES;
}
- (void)animationDidEnd:(NSAnimation*)animation
{
if([animation isEqual:windowAnimation]) {
//Restore docking behavior
if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
[(id)[self window] setDockingEnabled:YES];
if (windowSlidOffScreenEdgeMask == AINoEdges) {
//When the window is offscreen, its horizontal autosizing can't occur. Size it now.
[contactListController contactListDesiredSizeChanged];
} else {
//Offscreen windows should be told not to cast a shadow
[[self window] setHasShadow:NO];
previousAlpha = [[self window] alphaValue];
[[self window] setAlphaValue:0.0];
AILogWithSignature(@"Previous alpha is now %f; window set to alpha 0.0 ", previousAlpha);
}
self.windowAnimation = nil;
}
if (animation == filterBarAnimation) {
if (filterBarIsVisible) {
// If the filter bar is already visible, remove it from its superview.
[filterBarView removeFromSuperview];
// Set the first responder back to the contact list view.
[[self window] makeFirstResponder:contactListView];
[contactListView selectItemsInArray:filterBarPreviouslySelected];
// Since this wasn't a user-initiated selection change, we need to post a notification for it.
[[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactSelectionChanged
object:nil];
[filterBarPreviouslySelected release]; filterBarPreviouslySelected = nil;
filterBarIsVisible = NO;
} else {
// If the filter bar wasn't visible, make it the first responder.
[[self window] makeFirstResponder:searchField];
// Set the filter bar as the next responder so the chain works for things like the info inspector
[filterBarView setNextResponder:contactListView];
// Bring the contact list to front, in case the find command was triggered from another window like the info inspector
[[self window] makeKeyAndOrderFront:nil];
filterBarPreviouslySelected = [[contactListView arrayOfSelectedItems] retain];
filterBarIsVisible = YES;
}
// Let the contact list controller know that our size has changed.
[contactListController contactListDesiredSizeChanged];
// We're no longer animating.
self.filterBarAnimation = nil;
}
}
- (BOOL)keepListOnScreenWhenSliding
{
return NO;
}
/*!
* @brief Slide the window to a given point
*
* windowSlidOffScreenEdgeMask must already be set to the resulting offscreen mask (or 0 if the window is sliding on screen)
*
* A standard window (titlebar window) will crash if told to setFrame completely offscreen. Also, using our own movement we can more precisely
* control the movement speed and acceleration.
*/
- (void)slideWindowToPoint:(NSPoint)targetPoint
{
NSWindow *myWindow = [self window];
NSScreen *windowScreen;
windowScreen = [myWindow screen];
if (!windowScreen) windowScreen = [self windowLastScreen];
if (!windowScreen) windowScreen = [NSScreen mainScreen];
NSRect frame = [myWindow frame];
CGFloat yOff = (targetPoint.y + NSHeight(frame)) - NSMaxY([windowScreen frame]);
if (windowScreen == [[NSScreen screens] objectAtIndex:0]) yOff -= [[NSApp mainMenu] menuBarHeight];
if (yOff > 0) targetPoint.y -= yOff;
frame.origin = targetPoint;
if ((windowSlidOffScreenEdgeMask != AINoEdges) &&
[self keepListOnScreenWhenSliding]) {
switch (windowSlidOffScreenEdgeMask) {
case AIMinXEdgeMask:
frame.origin.x += 1;
break;
case AIMaxXEdgeMask:
frame.origin.x -= 1;
break;
case AIMaxYEdgeMask:
frame.origin.y -= 1;
break;
case AIMinYEdgeMask:
frame.origin.y += 1;
break;
case AINoEdges:
//We'll never get here
break;
}
}
if (windowAnimation) {
[windowAnimation stopAnimation];
self.windowAnimation = nil;
}
self.windowAnimation = [[[NSViewAnimation alloc] initWithViewAnimations:
[NSArray arrayWithObject:
[NSDictionary dictionaryWithObjectsAndKeys:
myWindow, NSViewAnimationTargetKey,
[NSValue valueWithRect:frame], NSViewAnimationEndFrameKey,
nil]]] autorelease];
[windowAnimation setFrameRate:0.0];
[windowAnimation setDuration:0.25];
[windowAnimation setDelegate:self];
[windowAnimation setAnimationBlockingMode:NSAnimationNonblocking];
[windowAnimation startAnimation];
}
- (void)moveWindowToPoint:(NSPoint)inOrigin
{
[[self window] setFrameOrigin:inOrigin];
if (windowSlidOffScreenEdgeMask == AINoEdges) {
/* When the window is offscreen, there are no constraints on its size, for example it will grow downwards as much as
* it needs to to accomodate new rows. Now that it's onscreen, there are constraints.
*/
[contactListController contactListDesiredSizeChanged];
[[self window] setAlphaValue:previousAlpha];
AILogWithSignature(@"Set window to previous alpha of %f", previousAlpha);
}
}
static BOOL AIScreenRectEdgeAdjacentToAnyOtherScreen(NSRectEdge edge, NSScreen *screen)
{
NSArray *screens = [NSScreen screens];
NSUInteger numScreens = [screens count];
if (numScreens > 1) {
NSRect screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:screen]] rectValue];
NSRect shiftedScreenFrame = screenSlideBoundaryRect;
BOOL isAdjacent = NO;
switch(edge) {
case NSMinXEdge:
shiftedScreenFrame.origin.x -= 1;
break;
case NSMinYEdge:
shiftedScreenFrame.origin.y -= 1;
break;
case NSMaxXEdge:
shiftedScreenFrame.size.width += 1;
break;
case NSMaxYEdge:
shiftedScreenFrame.size.height += 1;
break;
}
for (NSInteger i = 0; i < numScreens; i++) {
NSScreen *otherScreen = [screens objectAtIndex:i];
if (otherScreen != screen) {
if (NSIntersectsRect([otherScreen frame], shiftedScreenFrame)) {
isAdjacent = YES;
break;
}
}
}
return isAdjacent;
} else {
return NO;
}
}
/*!
* @brief Find the mask specifying what edges are potentially slidable for our window
*
* @result AIRectEdgeMask, which is 0 if no edges are slidable
*/
- (AIRectEdgeMask)slidableEdgesAdjacentToWindow
{
AIRectEdgeMask slidableEdges = 0;
NSWindow *window = [self window];
NSRect windowFrame = [window frame];
NSScreen *windowScreen = [window screen];
NSRect screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowScreen]] rectValue];
NSRectEdge edge;
for (edge = 0; edge < 4; edge++) {
if ((SLIDE_ALLOWED_RECT_EDGE_MASK & (1 << edge)) &&
(AIRectIsAligned_edge_toRect_edge_tolerance_(windowFrame,
edge,
screenSlideBoundaryRect,
edge,
WINDOW_ALIGNMENT_TOLERANCE)) &&
(!AIScreenRectEdgeAdjacentToAnyOtherScreen(edge, windowScreen))) {
slidableEdges |= (1 << edge);
}
}
return slidableEdges;
}
- (void)slideWindowOffScreenEdges:(AIRectEdgeMask)rectEdgeMask
{
NSWindow *window;
NSRect newWindowFrame;
NSRectEdge edge;
if (rectEdgeMask == AINoEdges)
return;
window = [self window];
newWindowFrame = [window frame];
[self setSavedFrame:newWindowFrame];
[windowLastScreen release];
windowLastScreen = [[window screen] retain];
NSRect screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowLastScreen]] rectValue];
for (edge = 0; edge < 4; edge++) {
if (rectEdgeMask & (1 << edge)) {
newWindowFrame = AIRectByAligningRect_edge_toRect_edge_(newWindowFrame,
AIOppositeRectEdge_(edge),
screenSlideBoundaryRect,
edge);
}
}
windowSlidOffScreenEdgeMask |= rectEdgeMask;
[self slideWindowToPoint:newWindowFrame.origin];
}
- (void)slideWindowOnScreenWithAnimation:(BOOL)animate
{
if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
NSWindow *window = [self window];
animate = animate && !NSEqualRects(window.frame, oldFrame);
//Restore shadow and frame if we're appearing from having slid off-screen
[window setHasShadow:[[adium.preferenceController preferenceForKey:KEY_CL_WINDOW_HAS_SHADOW
group:PREF_GROUP_CONTACT_LIST] boolValue]];
[window orderFront:nil];
[contactListController contactListWillSlideOnScreen];
windowSlidOffScreenEdgeMask = AINoEdges;
if (animate) {
[self slideWindowToPoint:oldFrame.origin];
} else {
[self moveWindowToPoint:oldFrame.origin];
}
[windowLastScreen release]; windowLastScreen = nil;
}
}
- (void)slideWindowOnScreen
{
[self slideWindowOnScreenWithAnimation:YES];
}
- (void)setPreventHiding:(BOOL)newPreventHiding {
preventHiding = newPreventHiding;
}
- (void)endWindowSlidingDelay
{
[self setPreventHiding:NO];
}
- (void)delayWindowSlidingForInterval:(NSTimeInterval)inDelayTime
{
[self setPreventHiding:YES];
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(endWindowSlidingDelay)
object:nil];
[self performSelector:@selector(endWindowSlidingDelay)
withObject:nil
afterDelay:inDelayTime];
}
- (AIRectEdgeMask)windowSlidOffScreenEdgeMask
{
return windowSlidOffScreenEdgeMask;
}
// Snap Groups Together------------------------------------------------------------------------------------------------
#pragma mark Snap Groups Together
/*!
* @brief If window did move and is not docked then snap it to other windows
*/
- (void)windowDidMove:(NSNotification *)notification
{
BOOL suppressSnapping = [NSEvent shiftKey];
attachToBottom = nil;
if (windowSlidOffScreenEdgeMask == AINoEdges && !suppressSnapping)
[self snapToOtherWindows];
}
/*!
* @brief Captures mouse up event to check that if the window snapped underneath
* another window they are merged together
*/
- (void)mouseUp:(NSEvent *)event {
if (attachToBottom) {
AIContactList *from = (AIContactList *)[self contactList];
AIContactList *to = (AIContactList *)[attachToBottom contactList];
for (AIListGroup *group in from) {
[adium.contactController moveGroup:group fromContactList:from toContactList:to];
}
[[NSNotificationCenter defaultCenter] postNotificationName:DetachedContactListIsEmpty
object:from
userInfo:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:@"Contact_ListChanged"
object:to
userInfo:nil];
}
[super mouseUp:event];
}
/*!
* @brief Snaps window to windows next to it
*/
- (void)snapToOtherWindows
{
NSWindow *myWindow = [self window];
NSArray *windows = [[NSApplication sharedApplication] windows];
NSWindow *window;
NSRect currentFrame = [myWindow frame];
NSPoint suggested = currentFrame.origin;
// Check to snap to each guide
for (window in windows) {
// No snapping to itself and it must be within a snapping distance to other windows
if ((window != myWindow) &&
[window delegate] && [window isVisible] &&
[[window delegate] conformsToProtocol:@protocol(AIInterfaceContainer)]) {
/* Note: [window delegate] may be invalid if the window is in the middle of closing.
* Checking if it's visible should hopefully cover that case.
*/
suggested = [self snapTo:window with:currentFrame saveTo:suggested];
}
}
[[self window] setFrameOrigin:suggested];
}
/*!
* @brief Check that window is inside snappable region of other window
*/
static BOOL isInRangeOfRect(NSRect sourceRect, NSRect targetRect)
{
return NSIntersectsRect(NSInsetRect(sourceRect, -SNAP_DISTANCE, -SNAP_DISTANCE), targetRect);
}
/*!
* @brief Check if points are close enough to be snapped together
*/
static BOOL canSnap(CGFloat a, CGFloat b)
{
return (abs(a - b) <= SNAP_DISTANCE);
}
- (NSPoint)snapTo:(NSWindow*)neighborWindow with:(NSRect)currentRect saveTo:(NSPoint)location{
NSRect neighbor = [neighborWindow frame];
NSPoint spacing = [self windowSpacing];
NSUInteger overlap = 0;
NSUInteger bottom = 0;
if (!NSEqualRects(neighbor,currentRect) && isInRangeOfRect(currentRect, neighbor)) {
// X Snapping
if (canSnap(NSMaxX(currentRect), NSMinX(neighbor))) {
location.x = NSMinX(neighbor) - NSWidth(currentRect) - spacing.x;
} else if (canSnap(NSMinX(currentRect), NSMaxX(neighbor))) {
location.x = NSMaxX(neighbor) + spacing.x;
} else if (canSnap(NSMinX(currentRect), NSMinX(neighbor))) {
location.x = NSMinX(neighbor);
overlap++;
bottom++;
}
// Y Snapping
if (canSnap(NSMaxY(neighbor), NSMaxY(currentRect))) {
location.y = NSMaxY(neighbor) - NSHeight(currentRect);
overlap++;
} else if (canSnap(NSMinY(neighbor), NSMaxY(currentRect))) {
location.y = NSMinY(neighbor) - NSHeight(currentRect) - spacing.y;
bottom++;
} else if (canSnap(NSMaxY(neighbor), NSMinY(currentRect))) {
location.y = NSMaxY(neighbor) + spacing.y;
} else if (canSnap(NSMinY(neighbor), NSMinY(currentRect))) {
location.y = NSMinY(neighbor);
overlap++;
}
}
// If we snapped on top of neighbor
if (overlap == 2)
return currentRect.origin;
// Save window that we could possible attach to
if (bottom == 2)
attachToBottom = [neighborWindow delegate];
return location;
}
/*!
* @brief Gets space that windows should be apart by based on current window style
*/
- (NSPoint)windowSpacing {
AIContactListWindowStyle style = [[adium.preferenceController preferenceForKey:KEY_LIST_LAYOUT_WINDOW_STYLE
group:PREF_GROUP_APPEARANCE] integerValue];
NSInteger space = [[adium.preferenceController preferenceForKey:@"Group Top Spacing"
group:@"List Layout"] integerValue];
switch (style) {
case AIContactListWindowStyleStandard:
case AIContactListWindowStyleBorderless:
case AIContactListWindowStyleGroupChat:
return NSMakePoint(0,0);
case AIContactListWindowStyleGroupBubbles:
case AIContactListWindowStyleContactBubbles:
case AIContactListWindowStyleContactBubbles_Fitted:
return NSMakePoint(space,space-WINDOW_ALIGNMENT_TOLERANCE);
}
return NSMakePoint(0,0);
}
#pragma mark Filtering
/*!
* @brief Toggles the find bar on, or brings it into focus if it is already visible
*/
- (void)toggleFindPanel:(id)sender;
{
if (filterBarIsVisible) {
[[self window] makeFirstResponder:searchField];
} else if ([contactListView numberOfRows] > 0) {
filterBarShownAutomatically = NO;
[self showFilterBarWithAnimation:YES];
} else {
NSBeep();
}
}
/*!
* @brief Hide the filter bar
*/
- (IBAction)hideFilterBar:(id)sender;
{
[self hideFilterBarWithAnimation:YES];
}
/*!
* @brief Show the filter bar
*
* @param useAnimation If YES, the filter bar will scroll into view, otherwise it appears immediately
*/
- (void)showFilterBarWithAnimation:(BOOL)useAnimation
{
if (filterBarIsVisible || filterBarAnimation)
return;
// While the filter bar is shown, temporarily disable automatic horizontal resizing
contactListController.autoResizeHorizontally = NO;
// Disable contact list animation while the filter bar is shown
[contactListView setEnableAnimation:NO];
// Animate the filter bar into view
[self animateFilterBarWithDuration:(useAnimation ? 0.15f : 0.0)];
}
/*!
* @brief Hide the filter bar
*
* @param useAnimation If YES, the filter bar will scroll out of view, otherwise it disappears immediately
*/
- (void)hideFilterBarWithAnimation:(BOOL)useAnimation
{
if (!filterBarIsVisible || filterBarAnimation)
return;
// Clear the search field so that visibility is reset
[searchField setStringValue:@""];
[self filterContacts:searchField];
// Restore the default settings which we temporarily disabled previously
contactListController.autoResizeHorizontally = [[adium.preferenceController preferenceForKey:KEY_LIST_LAYOUT_HORIZONTAL_AUTOSIZE group:PREF_GROUP_APPEARANCE] boolValue];
[contactListView setEnableAnimation:[[adium.preferenceController preferenceForKey:KEY_CL_ANIMATE_CHANGES
group:PREF_GROUP_CONTACT_LIST] boolValue]];
// Animate the filter bar out of view
[self animateFilterBarWithDuration:(useAnimation ? 0.15f : 0.0)];
}
/*!
* @brief Animates the filter bar in and out of view
*
* @param duration The duration the animation will last
*/
- (void)animateFilterBarWithDuration:(CGFloat)duration
{
NSView *targetView = ([contactListView enclosingScrollView] ? (NSView *)[contactListView enclosingScrollView] : contactListView);
NSRect targetFrame = [targetView frame];
NSDictionary *targetViewDict, *filterBarDict;
// Contact list resizing
if (filterBarIsVisible) {
targetFrame.size.height = NSHeight(targetFrame) + NSHeight([filterBarView bounds]);
} else {
/* We can only have a height less than the filter bar view if we are autosizing vertically, as
* there is a minimum height otherwise which is larger. We can therefore increase our window size to allow space
* for the filter bar with impunity and without undoing this when hiding the bar, as the autosizing of the contact
* list will get us back to the right size later.
*/
if (NSHeight(targetFrame) < (NSHeight([filterBarView bounds]) * 2)) {
NSRect windowFrame = [[targetView window] frame];
[[targetView window] setFrame:NSMakeRect(NSMinX(windowFrame), NSMinY(windowFrame) - NSHeight([filterBarView bounds]),
NSWidth(windowFrame), NSHeight(windowFrame) + NSHeight([filterBarView bounds]))
display:NO
animate:NO];
targetFrame = [targetView frame];
}
targetFrame.size.height = NSHeight(targetFrame) - NSHeight([filterBarView bounds]);
}
/* Setting a frame's height to 0 can permanently destroy its ability to display properly.
* This is the case with an NSOutlineView. If our contact list was invisibile (because no contacts
* were visible), create a 1 pixel border rather than traumatizing it for life.
*/
if (targetFrame.size.height == 0)
targetFrame.size.height = 1;
// Filter bar resizing
NSRect barTargetFrame = contactListView.enclosingScrollView.frame;
if (filterBarIsVisible) {
barTargetFrame.size.height = NSHeight(barTargetFrame) + NSHeight(filterBarView.bounds);
} else {
barTargetFrame.size.height = NSHeight(barTargetFrame) - NSHeight(filterBarView.bounds);
}
if (!filterBarIsVisible) {
// If the filter bar isn't already visible
[filterBarView setFrame:NSMakeRect(NSMinX(barTargetFrame),
NSHeight([contactListView frame]),
NSWidth(barTargetFrame),
NSHeight([filterBarView bounds]))];
// Attach the filter bar to the window
[[[self window] contentView] addSubview:filterBarView];
}
filterBarDict = [NSDictionary dictionaryWithObjectsAndKeys:filterBarView, NSViewAnimationTargetKey,
[NSValue valueWithRect:NSMakeRect(NSMinX(barTargetFrame), NSHeight(barTargetFrame),
NSWidth(barTargetFrame), NSHeight([filterBarView bounds]))], NSViewAnimationEndFrameKey, nil];
targetViewDict = [NSDictionary dictionaryWithObjectsAndKeys:targetView, NSViewAnimationTargetKey,
[NSValue valueWithRect:targetFrame], NSViewAnimationEndFrameKey, nil];
self.filterBarAnimation = [[[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects:
targetViewDict,
filterBarDict,
nil]] autorelease];
[filterBarAnimation setDuration:duration];
[filterBarAnimation setAnimationBlockingMode:NSAnimationBlocking];
[filterBarAnimation setDelegate:self];
// Start the animation
[filterBarAnimation startAnimation];
}
/*!
* @brief Called when the window loses focus
*/
- (void)windowDidResignMain:(NSNotification *)sender
{
/* If the filter bar was shown by type-to-find (but not by command-F), and the window is no longer main,
* assume the user is done and hide the filter bar.
*/
if (filterBarIsVisible && filterBarShownAutomatically)
[self hideFilterBarWithAnimation:NO];
}
/*!
* @brief Forward typing events from the contact list to the filter bar
*/
- (BOOL)forwardKeyEventToFindPanel:(NSEvent *)theEvent;
{
//if we were not searching something before, we need to show the filter bar first without animation
NSString *charString = [theEvent charactersIgnoringModifiers];
unichar pressedChar = 0;
//Get the pressed character
if ([charString length] == 1) pressedChar = [charString characterAtIndex:0];
#define NSEscapeFunctionKey 27
/* Hitting escape once should clear any existing selection. Keys with functional modifiers pressed should not be passed.
* Home and End should be passed to the find panel only if it is already visible.
*/
if (((pressedChar == NSEscapeFunctionKey) && ([contactListView selectedRow] != -1 || !filterBarIsVisible)) ||
(([theEvent modifierFlags] & NSCommandKeyMask) || ([theEvent modifierFlags] & NSAlternateKeyMask) || ([theEvent modifierFlags] & NSControlKeyMask)) ||
((pressedChar == NSPageUpFunctionKey) || (pressedChar == NSPageDownFunctionKey) || (pressedChar == NSMenuFunctionKey)) ||
(!filterBarIsVisible && ((pressedChar == NSHomeFunctionKey) || (pressedChar == NSEndFunctionKey)))) {
return NO;
} else {
if (!filterBarIsVisible) {
/* Typing caused the filter bar ot be shown automatically */
filterBarShownAutomatically = YES;
[self showFilterBarWithAnimation:NO];
}
[[self window] makeFirstResponder:searchField];
[[[self window] fieldEditor:YES forObject:searchField] keyDown:theEvent];
return YES;
}
}
/*!
* @brief Process text commands while on the search field
*/
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
{
// Only process commands when we're in the search field.
if (control != searchField)
return NO;
if (command == @selector(insertNewline:)) {
// If we have a search term, open a chat with the first contact
if (![[textView string] isEqualToString:@""])
[self performDefaultActionOnSelectedObject:[contactListView firstVisibleListContact]
sender:contactListView];
// Hide the filter bar
[self hideFilterBarWithAnimation:YES];
} else if(command == @selector(moveDown:)) {
// The down arrow functions to move into the contact list view
[[self window] makeFirstResponder:contactListView];
} else if(command == @selector(cancelOperation:)) {
// Escape hides the filter bar.
[self hideFilterBarWithAnimation:YES];
} else {
// If we didn't process a command, return NO.
return NO;
}
// We processed a command, return YES.
return YES;
}
/*!
* @brief Filter contacts from the search field
*
* This method will expand or contract groups as necessary, as well as handle forwarding the search term to
* the contact hiding controller.
*/
- (IBAction)filterContacts:(id)sender;
{
if (![sender isKindOfClass:[NSSearchField class]])
return;
if (!filterBarExpandedGroups && ![[sender stringValue] isEqualToString:@""]) {
BOOL modified = NO;
for (AIListObject *listObject in [self.contactList containedObjects]) {
if ([listObject isKindOfClass:[AIListGroup class]] && [(AIListGroup *)listObject isExpanded] == NO) {
[listObject setValue:[NSNumber numberWithBool:YES] forProperty:@"ExpandedByFiltering" notify:NotifyNever];
modified = YES;
}
}
filterBarExpandedGroups = YES;
if (modified) {
[contactListView reloadData];
}
} else if (filterBarExpandedGroups && [[sender stringValue] isEqualToString:@""]) {
BOOL modified = NO;
for (AIListObject *listObject in [self.contactList containedObjects]) {
if ([listObject isKindOfClass:[AIListGroup class]] && [listObject boolValueForProperty:@"ExpandedByFiltering"]) {
[listObject setValue:[NSNumber numberWithBool:NO] forProperty:@"ExpandedByFiltering" notify:NotifyNever];
modified = YES;
}
}
filterBarExpandedGroups = NO;
if (modified) {
[contactListView reloadData];
}
}
if ([[AIContactHidingController sharedController] filterContacts:[sender stringValue]]) {
// Select the first contact; we're guaranteed at least one visible contact.
[contactListView selectRowIndexes:[NSIndexSet indexSetWithIndex:[contactListView indexOfFirstVisibleListContact]]
byExtendingSelection:NO];
// Since this wasn't a user-initiated selection change, we need to post a notification for it.
[[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactSelectionChanged
object:nil];
[[searchField cell] setTextColor:nil backgroundColor:nil];
} else {
//White on light red (like Firefox!)
[[searchField cell] setTextColor:[NSColor whiteColor] backgroundColor:[NSColor colorWithCalibratedHue:0.983
saturation:0.43
brightness:0.99
alpha:1.0]];
}
}
/*!
* @brief Delegate method for the search field's close button
*/
- (void)rolloverButton:(AIRolloverButton *)inButton mouseChangedToInsideButton:(BOOL)isInside
{
[button_cancelFilterBar setImage:[NSImage imageNamed:(isInside ? @"FTProgressStopRollover" : @"FTProgressStop")
forClass:[self class]]];
}
@end
|