Abhishek Banthia
3 years ago
60 changed files with 963 additions and 610 deletions
@ -0,0 +1,38 @@ |
|||||||
|
// Copyright © 2015 Abhishek Banthia |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
var avenirBookFont: NSFont { |
||||||
|
if let avenirFont = NSFont(name: "Avenir-Book", size: 13) { |
||||||
|
return avenirFont |
||||||
|
} |
||||||
|
return NSFont.systemFont(ofSize: 13) |
||||||
|
} |
||||||
|
|
||||||
|
protocol UpcomingEventPanelDelegate: AnyObject { |
||||||
|
func didRemoveCalendarView() |
||||||
|
func didClickSupplementaryButton(_ sender: NSButton) |
||||||
|
} |
||||||
|
|
||||||
|
extension ParentPanelController { |
||||||
|
func setupUpcomingEventViewCollectionViewIfNeccesary() { |
||||||
|
if upcomingEventCollectionView != nil { |
||||||
|
upcomingEventsDataSource = UpcomingEventsDataSource(self, EventCenter.sharedCenter()) |
||||||
|
upcomingEventCollectionView.enclosingScrollView?.scrollerInsets = NSEdgeInsetsZero |
||||||
|
upcomingEventCollectionView.enclosingScrollView?.backgroundColor = NSColor.clear |
||||||
|
upcomingEventCollectionView.setAccessibility("UpcomingEventCollectionView") |
||||||
|
upcomingEventCollectionView.dataSource = upcomingEventsDataSource |
||||||
|
upcomingEventCollectionView.delegate = upcomingEventsDataSource |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
extension ParentPanelController: UpcomingEventPanelDelegate { |
||||||
|
func didRemoveCalendarView() { |
||||||
|
removeUpcomingEventView() |
||||||
|
} |
||||||
|
|
||||||
|
func didClickSupplementaryButton(_ sender: NSButton) { |
||||||
|
calendarButtonAction(sender) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,131 @@ |
|||||||
|
// Copyright © 2015 Abhishek Banthia |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
class UpcomingEventViewItem: NSCollectionViewItem { |
||||||
|
static let reuseIdentifier = NSUserInterfaceItemIdentifier("UpcomingEventViewItem") |
||||||
|
|
||||||
|
@IBOutlet var calendarColorView: NSView! |
||||||
|
@IBOutlet var leadingConstraint: NSLayoutConstraint! |
||||||
|
@IBOutlet var eventTitleLabel: NSTextField! |
||||||
|
@IBOutlet var eventSubtitleButton: NSButton! |
||||||
|
@IBOutlet var supplementaryButtonWidthConstraint: NSLayoutConstraint! |
||||||
|
@IBOutlet var zoomButton: NSButton! |
||||||
|
|
||||||
|
private static let SupplementaryButtonWidth: CGFloat = 24.0 |
||||||
|
private static let EventLeadingConstraint: CGFloat = 10.0 |
||||||
|
private var meetingLink: URL? |
||||||
|
private weak var panelDelegate: UpcomingEventPanelDelegate? |
||||||
|
|
||||||
|
override func viewDidLoad() { |
||||||
|
zoomButton.target = self |
||||||
|
zoomButton.action = #selector(zoomButtonAction(_:)) |
||||||
|
} |
||||||
|
|
||||||
|
override func prepareForReuse() { |
||||||
|
zoomButton.image = nil |
||||||
|
eventTitleLabel.stringValue = "" |
||||||
|
eventSubtitleButton.stringValue = "" |
||||||
|
} |
||||||
|
|
||||||
|
override var acceptsFirstResponder: Bool { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: Setup UI |
||||||
|
func setup(_ title: String, |
||||||
|
_ subtitle: String, |
||||||
|
_ color: NSColor, |
||||||
|
_ link: URL?, |
||||||
|
_ delegate: UpcomingEventPanelDelegate?, |
||||||
|
_ isCancelled: Bool) { |
||||||
|
if leadingConstraint.constant != UpcomingEventViewItem.EventLeadingConstraint / 2 { |
||||||
|
leadingConstraint.animator().constant = UpcomingEventViewItem.EventLeadingConstraint / 2 |
||||||
|
} |
||||||
|
|
||||||
|
panelDelegate = delegate |
||||||
|
meetingLink = link |
||||||
|
|
||||||
|
setupLabels(title, isCancelled) |
||||||
|
setupSupplementaryButton(link) |
||||||
|
setCalendarButtonTitle(buttonTitle: subtitle) |
||||||
|
calendarColorView.layer?.backgroundColor = color.cgColor |
||||||
|
} |
||||||
|
|
||||||
|
private func setupLabels(_ title: String, _ cancellationState: Bool) { |
||||||
|
let attributes: [NSAttributedString.Key: Any] = cancellationState ? [NSAttributedString.Key.strikethroughStyle: NSUnderlineStyle.single.rawValue, |
||||||
|
NSAttributedString.Key.strikethroughColor: NSColor.gray] : [:] |
||||||
|
let attributedString = NSAttributedString(string: title, attributes: attributes) |
||||||
|
eventTitleLabel.attributedStringValue = attributedString |
||||||
|
|
||||||
|
eventTitleLabel.toolTip = title |
||||||
|
} |
||||||
|
|
||||||
|
private func setupSupplementaryButton(_ meetingURL: URL?) { |
||||||
|
guard meetingURL != nil else { |
||||||
|
zoomButton.image = nil |
||||||
|
supplementaryButtonWidthConstraint.constant = 0.0 |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
zoomButton.isHidden = false |
||||||
|
zoomButton.image = Themer.shared().videoCallImage() |
||||||
|
|
||||||
|
if supplementaryButtonWidthConstraint.constant != UpcomingEventViewItem.SupplementaryButtonWidth { |
||||||
|
supplementaryButtonWidthConstraint.constant = UpcomingEventViewItem.SupplementaryButtonWidth |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func setupUndeterminedState(_ delegate: UpcomingEventPanelDelegate?) { |
||||||
|
panelDelegate = delegate |
||||||
|
setAlternateState(NSLocalizedString("See your next Calendar event here.", comment: "Next Event Label for no Calendar access"), |
||||||
|
NSLocalizedString("Click here to start.", comment: "Button Title for no Calendar access"), |
||||||
|
NSColor.systemBlue, |
||||||
|
Themer.shared().removeImage()) |
||||||
|
} |
||||||
|
|
||||||
|
func setupEmptyState() { |
||||||
|
setAlternateState(NSLocalizedString("No upcoming events for today!", comment: "Next Event Label with no upcoming event"), |
||||||
|
NSLocalizedString("Great going.", comment: "Button Title for no upcoming event"), |
||||||
|
NSColor.systemGreen, |
||||||
|
nil) |
||||||
|
} |
||||||
|
|
||||||
|
private func setAlternateState(_ title: String, _ buttonTitle: String, _ color: NSColor, _ image: NSImage? = nil) { |
||||||
|
if leadingConstraint.constant != UpcomingEventViewItem.EventLeadingConstraint { |
||||||
|
leadingConstraint.animator().constant = UpcomingEventViewItem.EventLeadingConstraint |
||||||
|
} |
||||||
|
|
||||||
|
eventTitleLabel.stringValue = title |
||||||
|
setCalendarButtonTitle(buttonTitle: buttonTitle) |
||||||
|
calendarColorView.layer?.backgroundColor = color.cgColor |
||||||
|
zoomButton.image = image |
||||||
|
} |
||||||
|
|
||||||
|
private func setCalendarButtonTitle(buttonTitle: String) { |
||||||
|
let style = NSMutableParagraphStyle() |
||||||
|
style.alignment = .left |
||||||
|
style.lineBreakMode = .byTruncatingTail |
||||||
|
|
||||||
|
if let boldFont = NSFont(name: "Avenir", size: 11) { |
||||||
|
let attributes = [NSAttributedString.Key.foregroundColor: NSColor.gray, NSAttributedString.Key.paragraphStyle: style, NSAttributedString.Key.font: boldFont] |
||||||
|
let attributedString = NSAttributedString(string: buttonTitle, attributes: attributes) |
||||||
|
eventSubtitleButton.attributedTitle = attributedString |
||||||
|
eventSubtitleButton.toolTip = attributedString.string |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// MARK: Button Actions |
||||||
|
|
||||||
|
@IBAction func calendarButtonAction(_ sender: NSButton) { |
||||||
|
panelDelegate?.didClickSupplementaryButton(sender) |
||||||
|
} |
||||||
|
|
||||||
|
@objc func zoomButtonAction(_: Any) { |
||||||
|
if let meetingURL = meetingLink { |
||||||
|
NSWorkspace.shared.open(meetingURL) |
||||||
|
} else { |
||||||
|
panelDelegate?.didRemoveCalendarView() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,99 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> |
||||||
|
<dependencies> |
||||||
|
<deployment identifier="macosx"/> |
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/> |
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> |
||||||
|
</dependencies> |
||||||
|
<objects> |
||||||
|
<customObject id="-2" userLabel="File's Owner"/> |
||||||
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> |
||||||
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/> |
||||||
|
<customView id="Hz6-mo-xeY"> |
||||||
|
<rect key="frame" x="0.0" y="0.0" width="250" height="75"/> |
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> |
||||||
|
<subviews> |
||||||
|
<customView translatesAutoresizingMaskIntoConstraints="NO" id="aM6-TI-1os" customClass="ClockerTextBackgroundView" customModule="Clocker" customModuleProvider="target"> |
||||||
|
<rect key="frame" x="0.0" y="0.0" width="250" height="75"/> |
||||||
|
<subviews> |
||||||
|
<button focusRingType="none" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Khp-8n-4FA"> |
||||||
|
<rect key="frame" x="16" y="21" width="198" height="18"/> |
||||||
|
<constraints> |
||||||
|
<constraint firstAttribute="height" constant="18" id="asu-Dd-XhW"/> |
||||||
|
</constraints> |
||||||
|
<buttonCell key="cell" type="bevel" title="All Day Event" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingHead" truncatesLastVisibleLine="YES" state="on" focusRingType="none" imageScaling="proportionallyDown" inset="2" id="JOt-0Q-EAr"> |
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> |
||||||
|
<font key="font" size="10" name="Avenir-Heavy"/> |
||||||
|
</buttonCell> |
||||||
|
<connections> |
||||||
|
<action selector="calendarButtonAction:" target="p0M-E4-898" id="Gua-lA-sNh"/> |
||||||
|
</connections> |
||||||
|
</button> |
||||||
|
<customView wantsLayer="YES" focusRingType="none" translatesAutoresizingMaskIntoConstraints="NO" id="Bbu-oG-vAb"> |
||||||
|
<rect key="frame" x="5" y="21" width="2" height="34"/> |
||||||
|
<constraints> |
||||||
|
<constraint firstAttribute="width" constant="2" id="Qhx-45-1Hc"/> |
||||||
|
<constraint firstAttribute="height" constant="34" id="xeh-m2-Fhh"/> |
||||||
|
</constraints> |
||||||
|
</customView> |
||||||
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" textCompletion="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fTt-gC-72b"> |
||||||
|
<rect key="frame" x="15" y="33" width="201" height="24"/> |
||||||
|
<constraints> |
||||||
|
<constraint firstAttribute="height" constant="24" id="BA4-1v-xiZ"/> |
||||||
|
</constraints> |
||||||
|
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="left" title="Nandita Jaiswal's 35th birthday" placeholderString="" id="Dtv-o3-gqd"> |
||||||
|
<font key="font" size="12" name="Avenir-Book"/> |
||||||
|
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> |
||||||
|
<color key="backgroundColor" red="1" green="0.5" blue="0.0" alpha="1" colorSpace="calibratedRGB"/> |
||||||
|
</textFieldCell> |
||||||
|
</textField> |
||||||
|
<button translatesAutoresizingMaskIntoConstraints="NO" id="qRe-Ao-1fr"> |
||||||
|
<rect key="frame" x="216" y="26" width="24" height="24"/> |
||||||
|
<constraints> |
||||||
|
<constraint firstAttribute="width" constant="24" id="ZRs-Vt-FOM"/> |
||||||
|
<constraint firstAttribute="height" constant="24" id="ouc-t4-DO2"/> |
||||||
|
</constraints> |
||||||
|
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="Appearance" imagePosition="only" alignment="center" imageScaling="proportionallyUpOrDown" inset="2" id="8Sv-kp-Zff"> |
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> |
||||||
|
<font key="font" metaFont="system"/> |
||||||
|
</buttonCell> |
||||||
|
</button> |
||||||
|
</subviews> |
||||||
|
<constraints> |
||||||
|
<constraint firstItem="qRe-Ao-1fr" firstAttribute="leading" secondItem="fTt-gC-72b" secondAttribute="trailing" constant="2" id="161-lb-rqc"/> |
||||||
|
<constraint firstItem="fTt-gC-72b" firstAttribute="top" secondItem="Bbu-oG-vAb" secondAttribute="top" constant="-2" id="FeN-R0-t1A"/> |
||||||
|
<constraint firstItem="Khp-8n-4FA" firstAttribute="bottom" secondItem="Bbu-oG-vAb" secondAttribute="bottom" id="LQD-54-bUh"/> |
||||||
|
<constraint firstItem="Bbu-oG-vAb" firstAttribute="leading" secondItem="aM6-TI-1os" secondAttribute="leading" constant="5" id="Myg-8W-znN"/> |
||||||
|
<constraint firstAttribute="trailing" secondItem="qRe-Ao-1fr" secondAttribute="trailing" constant="10" id="Myk-Ff-8WP"/> |
||||||
|
<constraint firstItem="Khp-8n-4FA" firstAttribute="trailing" secondItem="fTt-gC-72b" secondAttribute="trailing" id="URW-gt-WbF"/> |
||||||
|
<constraint firstItem="Khp-8n-4FA" firstAttribute="leading" secondItem="fTt-gC-72b" secondAttribute="leading" constant="-1" id="VyL-Kt-2aT"/> |
||||||
|
<constraint firstItem="fTt-gC-72b" firstAttribute="leading" secondItem="Bbu-oG-vAb" secondAttribute="trailing" constant="10" id="arv-Oe-yih"/> |
||||||
|
<constraint firstItem="qRe-Ao-1fr" firstAttribute="centerY" secondItem="aM6-TI-1os" secondAttribute="centerY" id="vdC-gb-1wa"/> |
||||||
|
<constraint firstItem="Bbu-oG-vAb" firstAttribute="centerY" secondItem="aM6-TI-1os" secondAttribute="centerY" id="wkl-Vo-0kU"/> |
||||||
|
</constraints> |
||||||
|
</customView> |
||||||
|
</subviews> |
||||||
|
<constraints> |
||||||
|
<constraint firstItem="aM6-TI-1os" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="2AJ-xz-nf9"/> |
||||||
|
<constraint firstItem="aM6-TI-1os" firstAttribute="leading" secondItem="Hz6-mo-xeY" secondAttribute="leading" id="D6T-PQ-RdE"/> |
||||||
|
<constraint firstAttribute="bottom" secondItem="aM6-TI-1os" secondAttribute="bottom" id="DmC-7T-lrT"/> |
||||||
|
<constraint firstAttribute="trailing" secondItem="aM6-TI-1os" secondAttribute="trailing" id="aCZ-sV-Mnd"/> |
||||||
|
</constraints> |
||||||
|
<point key="canvasLocation" x="147" y="238.5"/> |
||||||
|
</customView> |
||||||
|
<customObject id="p0M-E4-898" customClass="UpcomingEventViewItem" customModule="Clocker" customModuleProvider="target"> |
||||||
|
<connections> |
||||||
|
<outlet property="calendarColorView" destination="Bbu-oG-vAb" id="tBe-V1-qmC"/> |
||||||
|
<outlet property="eventSubtitleButton" destination="Khp-8n-4FA" id="mXV-EZ-2AW"/> |
||||||
|
<outlet property="eventTitleLabel" destination="fTt-gC-72b" id="bOh-LO-tiH"/> |
||||||
|
<outlet property="leadingConstraint" destination="Myg-8W-znN" id="OUR-Na-THS"/> |
||||||
|
<outlet property="supplementaryButtonWidthConstraint" destination="ZRs-Vt-FOM" id="S8K-B3-ZLb"/> |
||||||
|
<outlet property="view" destination="Hz6-mo-xeY" id="9S0-wg-csq"/> |
||||||
|
<outlet property="zoomButton" destination="qRe-Ao-1fr" id="0IC-Fx-l3Q"/> |
||||||
|
</connections> |
||||||
|
</customObject> |
||||||
|
</objects> |
||||||
|
<resources> |
||||||
|
<image name="Appearance" width="350" height="350"/> |
||||||
|
</resources> |
||||||
|
</document> |
@ -0,0 +1,64 @@ |
|||||||
|
// Copyright © 2015 Abhishek Banthia |
||||||
|
|
||||||
|
import Foundation |
||||||
|
|
||||||
|
class UpcomingEventsDataSource: NSObject, NSCollectionViewDataSource, NSCollectionViewDelegateFlowLayout { |
||||||
|
private var upcomingEvents: [EventInfo] = [] |
||||||
|
private var eventCenter: EventCenter! |
||||||
|
private weak var delegate: UpcomingEventPanelDelegate? |
||||||
|
private static let panelWidth: CGFloat = 350.0 |
||||||
|
|
||||||
|
init(_ panelDelegate: UpcomingEventPanelDelegate?, _ center: EventCenter) { |
||||||
|
super.init() |
||||||
|
delegate = panelDelegate |
||||||
|
eventCenter = center |
||||||
|
} |
||||||
|
|
||||||
|
func updateEventsDataSource(_ events: [EventInfo]) { |
||||||
|
upcomingEvents = events |
||||||
|
} |
||||||
|
|
||||||
|
func collectionView(_: NSCollectionView, numberOfItemsInSection _: Int) -> Int { |
||||||
|
if eventCenter.calendarAccessDenied() || eventCenter.calendarAccessNotDetermined() || upcomingEvents.isEmpty { |
||||||
|
return 1 |
||||||
|
} |
||||||
|
return upcomingEvents.count |
||||||
|
} |
||||||
|
|
||||||
|
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { |
||||||
|
let item = collectionView.makeItem(withIdentifier: UpcomingEventViewItem.reuseIdentifier, for: indexPath) as! UpcomingEventViewItem |
||||||
|
if eventCenter.calendarAccessNotDetermined() { |
||||||
|
item.setupUndeterminedState(delegate) |
||||||
|
return item |
||||||
|
} |
||||||
|
|
||||||
|
if upcomingEvents.isEmpty { |
||||||
|
item.setupEmptyState() |
||||||
|
return item |
||||||
|
} |
||||||
|
|
||||||
|
let currentEventInfo = upcomingEvents[indexPath.item] |
||||||
|
let upcomingEventSubtitle = currentEventInfo.isAllDay ? "All-Day" : currentEventInfo.metadataForMeeting() |
||||||
|
item.setup(currentEventInfo.event.title, |
||||||
|
upcomingEventSubtitle, |
||||||
|
currentEventInfo.event.calendar.color, |
||||||
|
currentEventInfo.meetingURL, |
||||||
|
delegate, |
||||||
|
currentEventInfo.event.status == .canceled) |
||||||
|
return item |
||||||
|
} |
||||||
|
|
||||||
|
func collectionView(_ collectionView: NSCollectionView, layout _: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { |
||||||
|
if eventCenter.calendarAccessNotDetermined() { |
||||||
|
return NSSize(width: UpcomingEventsDataSource.panelWidth - 25, height: collectionView.frame.height - 15) |
||||||
|
} else if upcomingEvents.isEmpty { |
||||||
|
return NSSize(width: UpcomingEventsDataSource.panelWidth - 25, height: collectionView.frame.height - 15) |
||||||
|
} else { |
||||||
|
let currentEventInfo = upcomingEvents[indexPath.item] |
||||||
|
let bufferWidth: CGFloat = currentEventInfo.meetingURL != nil ? 60.0 : 20.0 |
||||||
|
let attributedString = NSAttributedString(string: currentEventInfo.event.title, attributes: [NSAttributedString.Key.font: avenirBookFont]) |
||||||
|
let maxWidth = min(attributedString.size().width + bufferWidth, UpcomingEventsDataSource.panelWidth / 2) |
||||||
|
return NSSize(width: maxWidth, height: collectionView.frame.height - 20) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue