BallotResultMatrixView.m 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2014-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import "BallotResultMatrixView.h"
  21. #import "Conversation.h"
  22. #import "Contact.h"
  23. #import "BallotChoice.h"
  24. #import "BallotResult.h"
  25. #import "RectUtil.h"
  26. #import "MyIdentityStore.h"
  27. #import "BallotMatrixLabelView.h"
  28. #import "BallotResultMatrixCell.h"
  29. #import "SlaveScrollView.h"
  30. #import "ScrollViewContent.h"
  31. #import "PopoverView.h"
  32. #import "AvatarMaker.h"
  33. #import "BundleUtil.h"
  34. #define DEGREES_TO_RADIANS(x) (M_PI * (x) / 180.0)
  35. #define LABEL_RADIANS -0.9
  36. #define X_PADDING 8.0f
  37. #define TOP_PADDING 0.0f
  38. #define BOTTOM_PADDING 8.0f
  39. #define MATRIX_PADDING 0.0f
  40. #define BORDER_WIDTH 1.0
  41. #define BORDER_COLOR [Colors background]
  42. #define GRID_HEIGHT 36.0f
  43. #define GRID_WIDTH 34.0f
  44. #define TOTALS_WIDTH 36.0f
  45. #define LABEL_LENGTH_CONTACT 100.0f
  46. #define CONTACT_Y_OFFSET_CORRECTION -10.0f
  47. #define CONTACT_FONT_SIZE 14.0f
  48. #define CHOICE_FONT_SIZE 14.0f
  49. #define CONTACT_AVATAR_PADDING 2.0f
  50. #define CONTACT_AVATAR_SIZE GRID_WIDTH
  51. #define HIGHEST_VOTE_COLOR [Colors ballotHighestVote]
  52. #define ROW_COLOR_LIGHT [Colors ballotRowLight]
  53. #define ROW_COLOR_DARKER [Colors ballotRowDark]
  54. @interface BallotResultMatrixView () <PopoverViewDelegate>
  55. @property NSInteger numChoices;
  56. @property NSInteger numParticipants;
  57. @property NSMutableArray *participantIds;
  58. @property NSMutableArray *participantNames;
  59. @property NSMutableArray *participantAvatars;
  60. @property (nonatomic) CGRect matrixRect;
  61. @property CGFloat gridHeight;
  62. @property CGFloat gridWidth;
  63. @property CGFloat totalsWidth;
  64. @property CGFloat labelAngleRadians;
  65. @property CGFloat minChoiceLabelLength;
  66. @property CGFloat contactLabelLength;
  67. @property CGFloat contactLabelHeight;
  68. @property SlaveScrollView *choicesView;
  69. @property SlaveScrollView *contactsView;
  70. @property SlaveScrollView *matrixView;
  71. @property SlaveScrollView *totalsView;
  72. @property UIView *totalsHeaderView;
  73. @property CGPoint beginTouchPoint;
  74. @property CGPoint endTouchPoint;
  75. @property NSMutableArray *highestVotes;
  76. @property PopoverView *popoverView;
  77. @property UIPanGestureRecognizer *panGesture;
  78. @end
  79. @implementation BallotResultMatrixView
  80. - (instancetype)initWithFrame:(CGRect)frame
  81. {
  82. self = [super initWithFrame:frame];
  83. if (self) {
  84. _labelAngleRadians = LABEL_RADIANS;
  85. _gridWidth = GRID_WIDTH;
  86. _gridHeight = GRID_HEIGHT;
  87. CGFloat minSideLength = fminf(CGRectGetWidth(self.frame), CGRectGetHeight(self.frame));
  88. _minChoiceLabelLength = minSideLength/3.0;
  89. _contactLabelHeight = GRID_WIDTH;
  90. _contactLabelLength = LABEL_LENGTH_CONTACT;
  91. _totalsWidth = TOTALS_WIDTH;
  92. _endTouchPoint = CGPointMake(0.0, 0.0);
  93. _highestVotes = [NSMutableArray array];
  94. self.userInteractionEnabled = YES;
  95. _panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
  96. [self addGestureRecognizer:_panGesture];
  97. self.backgroundColor = [Colors background];
  98. }
  99. return self;
  100. }
  101. - (void)adaptLayoutToSize:(CGSize)size {
  102. [_popoverView dismiss];
  103. _popoverView = nil;
  104. _matrixRect = [self matrixRectForSize:size];
  105. _choicesView.frame = [self choicesRectForSize:size];
  106. CGRect newContactsRect = [self contactsRectForSize:size];
  107. if (CGSizeEqualToSize(newContactsRect.size, _contactsView.frame.size) == NO) {
  108. ScrollViewContent *matrixContent = [self makeMatrixView];
  109. [_matrixView setContent: matrixContent];
  110. ScrollViewContent *contactsContent = [self makeContactsViewForSize:size];
  111. [_contactsView setContent: contactsContent];
  112. [self updateLineColors];
  113. [self markHighestVotes];
  114. [self setNeedsLayout];
  115. }
  116. _contactsView.frame = newContactsRect;
  117. _matrixView.frame = _matrixRect;
  118. _totalsView.frame = [self totalsRectForSize:size];
  119. }
  120. - (void)adaptToInterfaceRotation {
  121. [self adaptLayoutToSize:self.frame.size];
  122. }
  123. - (void)adaptToSize:(CGSize)size {
  124. [self adaptLayoutToSize:size];
  125. }
  126. - (void)setBallot:(Ballot *)ballot {
  127. _ballot = ballot;
  128. [self updateParticipants];
  129. _numChoices = [_ballot.choicesSortedByOrder count];
  130. _numParticipants = [_participantIds count];
  131. [self drawDataForSize:self.frame.size];
  132. }
  133. - (CGRect)matrixRectForSize:(CGSize)size {
  134. CGFloat offsetLeft = X_PADDING + MATRIX_PADDING + [self choicesWidthForSize:size] + _totalsWidth;
  135. CGFloat offsetRight = X_PADDING;
  136. CGFloat width = size.width - offsetLeft - offsetRight;
  137. CGFloat contactsHeight = [self contactsHeightForSize:size];
  138. CGFloat height = size.height - TOP_PADDING - BOTTOM_PADDING - contactsHeight;
  139. return CGRectMake(offsetLeft, TOP_PADDING + contactsHeight, width, height);
  140. }
  141. - (CGRect)choicesRectForSize:(CGSize)size {
  142. CGFloat contactsHeight = [self contactsHeightForSize:size];
  143. CGFloat y = TOP_PADDING + contactsHeight;
  144. CGFloat height = size.height - TOP_PADDING - BOTTOM_PADDING - contactsHeight;
  145. return CGRectMake(X_PADDING, y, [self choicesWidthForSize:size], height);
  146. }
  147. - (CGRect)contactsRectForSize:(CGSize)size {
  148. return CGRectMake(_matrixRect.origin.x, TOP_PADDING, _matrixRect.size.width, [self contactsHeightForSize:size]);
  149. }
  150. - (CGRect)totalsRectForSize:(CGSize)size {
  151. return CGRectMake(X_PADDING + [self choicesWidthForSize:size], TOP_PADDING + [self contactsHeightForSize:size], _totalsWidth, _matrixRect.size.height);
  152. }
  153. - (CGFloat)sin {
  154. CGFloat absRad = ABS(_labelAngleRadians);
  155. return sinf(absRad);
  156. }
  157. - (CGFloat)cos {
  158. CGFloat absRad = ABS(_labelAngleRadians);
  159. return cosf(absRad);
  160. }
  161. - (CGFloat)choicesWidthForSize:(CGSize)size {
  162. CGFloat width = size.width - 2*X_PADDING - _totalsWidth - MATRIX_PADDING - [self contactsTotalWidth];
  163. // minimum choice length
  164. return fmaxf(width, _minChoiceLabelLength);
  165. }
  166. - (CGFloat)contactsHeightForSize:(CGSize)size {
  167. if (SYSTEM_IS_IPAD || size.height > size.width) {
  168. return _contactLabelLength * [self sin] + _contactLabelHeight * [self cos] + CONTACT_AVATAR_SIZE;
  169. } else {
  170. return CONTACT_AVATAR_SIZE;
  171. }
  172. }
  173. - (CGFloat)contactsWidth {
  174. if (SYSTEM_IS_IPAD || UIInterfaceOrientationIsPortrait([UIApplication sharedApplication].statusBarOrientation)) {
  175. return _contactLabelLength * [self cos] + _contactLabelHeight * [self sin];
  176. } else {
  177. return CONTACT_AVATAR_SIZE;
  178. }
  179. }
  180. - (CGFloat)contactsTotalWidth {
  181. return (_numParticipants - 1) * _gridWidth + [self contactsWidth];
  182. }
  183. - (void)drawDataForSize:(CGSize)size {
  184. _matrixRect = [self matrixRectForSize:size];
  185. ScrollViewContent *choicesContent = [self makeChoicesViewForSize:size];
  186. _choicesView = [[SlaveScrollView alloc] initWithFrame: [self choicesRectForSize:size]];
  187. _choicesView.horizontalScrollingEnabled = NO;
  188. [_choicesView.panGestureRecognizer requireGestureRecognizerToFail: _panGesture];
  189. [_choicesView setContent: choicesContent];
  190. [self addSubview:_choicesView];
  191. ScrollViewContent *contactsContent = [self makeContactsViewForSize:size];
  192. _contactsView = [[SlaveScrollView alloc] initWithFrame: [self contactsRectForSize:size]];
  193. [_contactsView.panGestureRecognizer requireGestureRecognizerToFail: _panGesture];
  194. [_contactsView setContent: contactsContent];
  195. [self addSubview:_contactsView];
  196. ScrollViewContent *matrixContent = [self makeMatrixView];
  197. _matrixView = [[SlaveScrollView alloc] initWithFrame: _matrixRect];
  198. [_matrixView.panGestureRecognizer requireGestureRecognizerToFail: _panGesture];
  199. [_matrixView setContent: matrixContent];
  200. [self addSubview:_matrixView];
  201. CGRect totalsRect = [self totalsRectForSize:size];
  202. ScrollViewContent *totalsContent = [self makeResultTotalsView];
  203. _totalsView = [[SlaveScrollView alloc] initWithFrame: totalsRect];
  204. [_totalsView.panGestureRecognizer requireGestureRecognizerToFail: _panGesture];
  205. [_totalsView setContent: totalsContent];
  206. [self addSubview:_totalsView];
  207. [self updateLineColors];
  208. [self markHighestVotes];
  209. [self setNeedsLayout];
  210. }
  211. - (void)updateLineColors {
  212. UIColor *color;
  213. for (NSInteger i = 0; i < _numChoices; i++) {
  214. if (i % 2 == 0) {
  215. color = ROW_COLOR_LIGHT;
  216. } else {
  217. color = ROW_COLOR_DARKER;
  218. }
  219. [_choicesView setColor:color forRowAt:i];
  220. [_totalsView setColor:color forRowAt:i];
  221. [_matrixView setColor:color forRowAt:i];
  222. }
  223. }
  224. - (void)markHighestVotes {
  225. for (NSNumber *indexNumber in _highestVotes) {
  226. NSInteger idx = indexNumber.integerValue;
  227. [_choicesView setColor:HIGHEST_VOTE_COLOR forRowAt:idx];
  228. [_totalsView setColor:HIGHEST_VOTE_COLOR forRowAt:idx];
  229. [_matrixView setColor:HIGHEST_VOTE_COLOR forRowAt:idx];
  230. if ([Colors getTheme] == ColorThemeLight || [Colors getTheme] == ColorThemeLightWork) {
  231. [_choicesView setTextColor:[Colors fontInverted] forRowAt:idx];
  232. [_totalsView setTextColor:[Colors fontInverted] forRowAt:idx];
  233. [_matrixView setTextColor:[Colors fontInverted] forRowAt:idx];
  234. }
  235. }
  236. }
  237. - (ScrollViewContent *)makeContactsViewForSize:(CGSize)size {
  238. CGFloat height = [self contactsHeightForSize:size];
  239. BOOL showLabel = height > CONTACT_AVATAR_SIZE;
  240. CGRect contactRect;
  241. CGFloat yOffsetAvatar;
  242. CGFloat totalWidth;
  243. if (showLabel) {
  244. contactRect = [self contactsLabelRectForSize:size];
  245. totalWidth = [self contactsTotalWidth];
  246. yOffsetAvatar = height - CONTACT_AVATAR_SIZE;
  247. } else {
  248. contactRect = CGRectMake(0.0, 0.0, CONTACT_AVATAR_SIZE, CONTACT_AVATAR_SIZE);
  249. totalWidth = _participantNames.count * CONTACT_AVATAR_SIZE;
  250. yOffsetAvatar = 0.0;
  251. }
  252. CGRect totalRect = CGRectMake(0.0, 0.0, totalWidth, height);
  253. ScrollViewContent *contactView = [[ScrollViewContent alloc] initWithFrame:totalRect];
  254. for (int i = 0; i < _participantNames.count; i++) {
  255. if (showLabel) {
  256. NSString *participant = _participantNames[i];
  257. contactRect = [RectUtil offsetRect:contactRect byX:_gridWidth byY:0.0];
  258. CGRect labelRect = [RectUtil offsetRect:contactRect byX:0 byY:-CONTACT_AVATAR_SIZE];
  259. BallotMatrixLabelView *contactLabel = [self rotatedLabelAt:labelRect withText:participant];
  260. contactLabel.font = [UIFont systemFontOfSize: CONTACT_FONT_SIZE];
  261. [contactView addSubview: contactLabel];
  262. }
  263. UIImage *avatar = _participantAvatars[i];
  264. UIImageView *contactAvatar = [[UIImageView alloc] initWithImage:avatar];
  265. CGRect avatarRect = CGRectMake(i*GRID_WIDTH, yOffsetAvatar, CONTACT_AVATAR_SIZE, CONTACT_AVATAR_SIZE);
  266. avatarRect = CGRectInset(avatarRect, CONTACT_AVATAR_PADDING, CONTACT_AVATAR_PADDING);
  267. contactAvatar.frame = avatarRect;
  268. contactAvatar.tag = i;
  269. [contactView addSubview: contactAvatar];
  270. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
  271. [contactAvatar addGestureRecognizer:tapGesture];
  272. contactAvatar.userInteractionEnabled = YES;
  273. }
  274. return contactView;
  275. }
  276. - (CGRect)contactsLabelRectForSize:(CGSize)size {
  277. CGFloat height = [self contactsHeightForSize:size];
  278. CGFloat width = [self contactsWidth];
  279. CGFloat rotationXOffset = (width - _contactLabelLength)/2.0 - _contactLabelHeight * [self sin] + CONTACT_Y_OFFSET_CORRECTION;
  280. CGFloat rotationYOffset = (height - _contactLabelHeight + CONTACT_AVATAR_SIZE)/2.0;
  281. return CGRectMake(rotationXOffset, rotationYOffset, _contactLabelLength, _contactLabelHeight);
  282. }
  283. - (BallotMatrixLabelView *)rotatedLabelAt:(CGRect)rect withText:(NSString*)string {
  284. CGFloat sin = [self sin];
  285. CGFloat yLabelOffset = rect.size.height - rect.size.height * sin;
  286. BallotMatrixLabelView *label = [BallotMatrixLabelView labelForString:string at:rect];
  287. [label offsetAndResizeHeight: yLabelOffset];
  288. label.transform = CGAffineTransformMakeRotation(_labelAngleRadians);
  289. return label;
  290. }
  291. - (ScrollViewContent *)makeChoicesViewForSize:(CGSize)size {
  292. CGFloat totalHeight = _numChoices * _gridHeight;
  293. CGRect totalRect = CGRectMake(0.0, 0.0, _minChoiceLabelLength, totalHeight);
  294. ScrollViewContent *choiceView = [[ScrollViewContent alloc] initWithFrame:totalRect];
  295. // create views & get max width
  296. CGFloat yOffset = 0.0;
  297. CGFloat maxWidth = 0.0;
  298. NSMutableArray *labelViews = [NSMutableArray array];
  299. for (BallotChoice *choice in _ballot.choicesSortedByOrder) {
  300. CGRect contactRect = CGRectMake(0.0, yOffset, _minChoiceLabelLength, _gridHeight);
  301. BallotMatrixLabelView *choiceLabel = [BallotMatrixLabelView labelForString:choice.name at:contactRect];
  302. choiceLabel.accessibilityValue = [self accessibilityValueForChoice:choice];
  303. choiceLabel.isAccessibilityElement = YES;
  304. choiceLabel.font = [UIFont systemFontOfSize: CHOICE_FONT_SIZE];
  305. choiceLabel.borderWidth = BORDER_WIDTH;
  306. choiceLabel.borderColor = BORDER_COLOR;
  307. choiceLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  308. [choiceLabel sizeToFit];
  309. [labelViews addObject: choiceLabel];
  310. yOffset += _gridHeight;
  311. maxWidth = fmaxf(maxWidth, CGRectGetWidth(choiceLabel.frame));
  312. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
  313. [choiceLabel addGestureRecognizer:tapGesture];
  314. }
  315. if (maxWidth < [self choicesWidthForSize:size]) {
  316. maxWidth = [self choicesWidthForSize:size];
  317. choiceView.frame = [RectUtil setWidthOf:choiceView.frame width:maxWidth];
  318. }
  319. choiceView.minWidth = maxWidth;
  320. // resize and insert
  321. for (BallotMatrixLabelView *label in labelViews) {
  322. label.frame = [RectUtil setWidthOf:label.frame width:maxWidth];
  323. [choiceView addSubview: label];
  324. }
  325. return choiceView;
  326. }
  327. - (NSString *)accessibilityValueForChoice:(BallotChoice *)choice {
  328. NSMutableString *participants = [NSMutableString string];
  329. for (NSString *identity in choice.participantIdsForResultsTrue) {
  330. NSInteger index = [_participantIds indexOfObject:identity];
  331. if (index != NSNotFound) {
  332. if (participants.length > 0) {
  333. [participants appendString:@", "];
  334. }
  335. [participants appendString:_participantNames[index]];
  336. }
  337. }
  338. NSString *votesCountFormat = NSLocalizedStringFromTable(@"ballot_votes_count", @"Ballot", nil);
  339. NSString *votesCount = [NSString stringWithFormat:votesCountFormat, [NSString stringWithFormat: @"%li", (long)[choice totalCountOfResultsTrue]]];
  340. // use commas to create a pause for voice over
  341. return [NSString stringWithFormat:@"%@, %@, %@", choice.name, votesCount, participants];
  342. }
  343. - (ScrollViewContent *)makeResultTotalsView {
  344. CGFloat height = _numChoices * _gridHeight;
  345. CGRect rect = CGRectMake(0.0, 0.0, _totalsWidth, height);
  346. ScrollViewContent *resultTotalsView = [[ScrollViewContent alloc] initWithFrame:rect];
  347. resultTotalsView.layer.borderWidth = 0.5;
  348. resultTotalsView.layer.borderColor = BORDER_COLOR.CGColor;
  349. CGFloat xOffset = 0.0;
  350. CGFloat yOffset = 0.0;
  351. NSInteger index = 0;
  352. NSInteger maxCount = 0;
  353. for (BallotChoice *choice in _ballot.choicesSortedByOrder) {
  354. NSInteger count = [choice totalCountOfResultsTrue];
  355. if (count > 0) {
  356. if (count == maxCount) {
  357. [_highestVotes addObject: [NSNumber numberWithInteger:index]];
  358. } else if (count > maxCount) {
  359. maxCount = count;
  360. [_highestVotes removeAllObjects];
  361. [_highestVotes addObject:[NSNumber numberWithInteger:index]];
  362. }
  363. }
  364. CGRect rect = CGRectMake(xOffset, yOffset, _totalsWidth, _gridHeight);
  365. rect = [RectUtil rect:rect centerHorizontalIn:resultTotalsView.bounds];
  366. NSString *text = [NSString stringWithFormat:@"%li", (long)count];
  367. BallotMatrixLabelView *label = [BallotMatrixLabelView labelForString:text at:rect];
  368. label.maxWidth = _totalsWidth;
  369. label.textAlignment = NSTextAlignmentRight;
  370. label.borderWidth = BORDER_WIDTH;
  371. label.borderColor = BORDER_COLOR;
  372. [resultTotalsView addSubview:label];
  373. yOffset += _gridHeight;
  374. index++;
  375. }
  376. return resultTotalsView;
  377. }
  378. - (ScrollViewContent *)makeMatrixView {
  379. CGFloat totalHeight = _numChoices * _gridHeight;
  380. CGFloat rowWidth = _numParticipants * _gridWidth;
  381. CGFloat totalWidth = [self contactsTotalWidth];
  382. CGRect rect = CGRectMake(0.0, 0.0, totalWidth, totalHeight);
  383. ScrollViewContent *matrixView = [[ScrollViewContent alloc] initWithFrame:rect];
  384. CGFloat xOffset = 0.0;
  385. CGFloat yOffset = 0.0;
  386. for (BallotChoice *choice in _ballot.choicesSortedByOrder) {
  387. CGRect rowRect = CGRectMake(0.0, yOffset, rowWidth, _gridHeight);
  388. UIView *rowView = [[UIView alloc] initWithFrame: rowRect];
  389. for (NSString *participantId in _participantIds) {
  390. CGRect rect = CGRectMake(xOffset, 0.0, _gridWidth, _gridHeight);
  391. BallotResultMatrixCell *result = [[BallotResultMatrixCell alloc] initWithFrame:rect];
  392. [result updateResultForChoice:choice andParticipant:participantId];
  393. result.borderWidth = BORDER_WIDTH;
  394. result.borderColor = BORDER_COLOR;
  395. [rowView addSubview:result];
  396. xOffset += _gridWidth;
  397. }
  398. [matrixView addSubview:rowView];
  399. yOffset += _gridHeight;
  400. xOffset = 0.0;
  401. }
  402. return matrixView;
  403. }
  404. - (void)updateParticipants {
  405. _participantIds = [NSMutableArray array];
  406. _participantNames = [NSMutableArray array];
  407. _participantAvatars = [NSMutableArray array];
  408. NSString *myIdentity = [MyIdentityStore sharedMyIdentityStore].identity;
  409. [_participantIds addObject:myIdentity];
  410. [_participantNames addObject:[BundleUtil localizedStringForKey:@"me"]];
  411. NSMutableDictionary *profilePicture = [[MyIdentityStore sharedMyIdentityStore] profilePicture];
  412. UIImage *image = [UIImage imageWithData:profilePicture[@"ProfilePicture"]];
  413. if (image) {
  414. [_participantAvatars addObject:[[AvatarMaker sharedAvatarMaker] maskedProfilePicture:image size:CONTACT_AVATAR_SIZE-2*CONTACT_AVATAR_PADDING]];
  415. } else {
  416. [_participantAvatars addObject:[[AvatarMaker sharedAvatarMaker] avatarForContact:nil size:CONTACT_AVATAR_SIZE-2*CONTACT_AVATAR_PADDING masked:YES]];
  417. }
  418. for (Contact *contact in _ballot.participants) {
  419. [_participantIds addObject:contact.identity];
  420. [_participantNames addObject:contact.displayName];
  421. [_participantAvatars addObject:[[AvatarMaker sharedAvatarMaker] avatarForContact:contact size:CONTACT_AVATAR_SIZE-2*CONTACT_AVATAR_PADDING masked:YES]];
  422. }
  423. }
  424. #pragma mark - touch handling
  425. - (void)pan:(UIPanGestureRecognizer *)gestureRecognizer {
  426. CGPoint position = [gestureRecognizer locationInView:self];
  427. switch (gestureRecognizer.state) {
  428. case UIGestureRecognizerStateBegan:
  429. _beginTouchPoint = position;
  430. break;
  431. case UIGestureRecognizerStateChanged:
  432. [self panChangedTo:position];
  433. break;
  434. case UIGestureRecognizerStateEnded:
  435. _endTouchPoint = _matrixView.position;
  436. break;
  437. default:
  438. break;
  439. }
  440. }
  441. - (void)panChangedTo:(CGPoint)position
  442. {
  443. CGPoint diffWithOffset = [self positionDiffFor:position];
  444. [self updateSlaveViewsToPosition:diffWithOffset];
  445. }
  446. - (void)updateSlaveViewsToPosition:(CGPoint)position {
  447. [_choicesView setPosition:position];
  448. [_totalsView setPosition:position];
  449. CGPoint matrixPos = [_matrixView setPosition:position];
  450. [_contactsView setPosition:matrixPos];
  451. }
  452. - (CGPoint)positionDiffFor:(CGPoint)position {
  453. CGPoint positionDiff = [self diffFromPoint:_beginTouchPoint toPoint:position];
  454. CGPoint diffWithOffset = [self addPoint:_endTouchPoint toPoint:positionDiff];
  455. return diffWithOffset;
  456. }
  457. - (CGPoint)diffFromPoint:(CGPoint)fromPoint toPoint:(CGPoint)toPoint {
  458. CGFloat x = fromPoint.x - toPoint.x;
  459. CGFloat y = fromPoint.y - toPoint.y;
  460. return CGPointMake(x, y);
  461. }
  462. - (CGPoint)addPoint:(CGPoint)point toPoint:(CGPoint)toPoint {
  463. CGFloat x = toPoint.x + point.x;
  464. CGFloat y = toPoint.y + point.y;
  465. return CGPointMake(x, y);
  466. }
  467. #pragma mark - UITapGestureRecognizer
  468. - (void)handleTap:(UITapGestureRecognizer *)sender
  469. {
  470. if (sender.state == UIGestureRecognizerStateEnded)
  471. {
  472. if ([sender.view isKindOfClass:[BallotMatrixLabelView class]]) {
  473. BallotMatrixLabelView *label = (BallotMatrixLabelView *)sender.view;
  474. CGPoint point = [self convertPoint:label.bounds.origin fromView:label];
  475. _popoverView = [PopoverView showPopoverAtPoint:point inView:self withTitle:nil withText:label.text delegate:self];
  476. } else if ([sender.view isKindOfClass:[UIImageView class]]) {
  477. UIImageView *avatarView = (UIImageView *)sender.view;
  478. NSInteger index = [self indexForAvatarImage:avatarView.image];
  479. if (index >= 0) {
  480. CGPoint point = [self convertPoint:avatarView.bounds.origin fromView:avatarView];
  481. point.x += avatarView.bounds.size.width/2.0;
  482. NSString *name = [_participantNames objectAtIndex:avatarView.tag];
  483. _popoverView = [PopoverView showPopoverAtPoint:point inView:self withTitle:nil withText:name delegate:self];
  484. }
  485. }
  486. }
  487. }
  488. - (NSInteger)indexForAvatarImage:(UIImage *)image {
  489. for (NSInteger i=0; i<[_participantAvatars count]; i++) {
  490. UIImage *avatar = [_participantAvatars objectAtIndex:i];
  491. if (image == avatar) {
  492. return i;
  493. }
  494. }
  495. return -1;
  496. }
  497. #pragma mark - Popover delegate
  498. - (void)popoverViewDidDismiss:(PopoverView *)popoverView {
  499. _popoverView = nil;
  500. }
  501. @end