LocalizedDateCache.mm   [plain text]


/*
 * Copyright (C) 2011 Apple Inc. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#import "config.h"
#import "LocalizedDateCache.h"

// FIXME: Rename this file to LocalizedDataCacheIOS.mm and consider removing this guard.
#if PLATFORM(IOS_FAMILY)

#import "FontCascade.h"
#import <CoreFoundation/CFNotificationCenter.h>
#import <math.h>
#import <wtf/Assertions.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/StdLibExtras.h>

using namespace std;

namespace WebCore {

LocalizedDateCache& localizedDateCache()
{
    static NeverDestroyed<LocalizedDateCache> cache;
    return cache;
}

static void _localeChanged(CFNotificationCenterRef, void*, CFStringRef, const void*, CFDictionaryRef)
{
    localizedDateCache().localeChanged();
}

LocalizedDateCache::LocalizedDateCache()
{
    // Listen to CF Notifications for locale change, and clear the cache when it does.
    CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), (void*)this, _localeChanged,
                                    kCFLocaleCurrentLocaleDidChangeNotification,
                                    NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
}

LocalizedDateCache::~LocalizedDateCache()
{
    // NOTE: Singleton does not expect to be deconstructed.
    CFNotificationCenterRemoveObserver(CFNotificationCenterGetLocalCenter(), (void*)this,
                                       kCFLocaleCurrentLocaleDidChangeNotification, NULL);
}

void LocalizedDateCache::localeChanged()
{
    m_maxWidthMap.clear();
    m_formatterMap.clear();
}

NSDateFormatter *LocalizedDateCache::formatterForDateType(DateComponents::Type type)
{
    int key = static_cast<int>(type);
    if (m_formatterMap.contains(key))
        return m_formatterMap.get(key).get();

    NSDateFormatter *dateFormatter = [createFormatterForType(type) autorelease];
    m_formatterMap.set(key, dateFormatter);
    return dateFormatter;
}

float LocalizedDateCache::maximumWidthForDateType(DateComponents::Type type, const FontCascade& font, const MeasureTextClient& measurer)
{
    int key = static_cast<int>(type);
    if (m_font == font) {
        if (m_maxWidthMap.contains(key))
            return m_maxWidthMap.get(key);
    } else {
        m_font = FontCascade(font);
        m_maxWidthMap.clear();
    }

    float calculatedMaximum = calculateMaximumWidth(type, measurer);
    m_maxWidthMap.set(key, calculatedMaximum);
    return calculatedMaximum;
}

NSDateFormatter *LocalizedDateCache::createFormatterForType(DateComponents::Type type)
{
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    NSLocale *currentLocale = [NSLocale currentLocale];
    [dateFormatter setLocale:currentLocale];

    switch (type) {
    case DateComponents::Invalid:
        ASSERT_NOT_REACHED();
        break;
    case DateComponents::Date:
        [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
        [dateFormatter setTimeStyle:NSDateFormatterNoStyle];
        [dateFormatter setDateStyle:NSDateFormatterMediumStyle];
        break;
    case DateComponents::DateTime:
        [dateFormatter setTimeZone:[NSTimeZone localTimeZone]];
        [dateFormatter setTimeStyle:NSDateFormatterShortStyle];
        [dateFormatter setDateStyle:NSDateFormatterMediumStyle];
        break;
    case DateComponents::DateTimeLocal:
        [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
        [dateFormatter setTimeStyle:NSDateFormatterShortStyle];
        [dateFormatter setDateStyle:NSDateFormatterMediumStyle];
        break;
    case DateComponents::Month:
        [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
        [dateFormatter setDateFormat:[NSDateFormatter dateFormatFromTemplate:@"MMMMyyyy" options:0 locale:currentLocale]];
        break;
    case DateComponents::Time:
        [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
        [dateFormatter setTimeStyle:NSDateFormatterShortStyle];
        [dateFormatter setDateStyle:NSDateFormatterNoStyle];
        break;
    case DateComponents::Week:
        ASSERT_NOT_REACHED();
        break;
    }

    return dateFormatter;
}

// NOTE: This does not check for the widest day of the week.
// We assume no formatter option shows that information.
float LocalizedDateCache::calculateMaximumWidth(DateComponents::Type type, const MeasureTextClient& measurer)
{
    float maximumWidth = 0;

    // Get the formatter we would use, copy it because we will force its time zone to be UTC.
    NSDateFormatter *dateFormatter = [[formatterForDateType(type) copy] autorelease];
    [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];

    // Sample date with a 4 digit year and 2 digit day, hour, and minute. Digits are
    // typically all equally wide. Force UTC timezone for the test date below so the
    // date doesn't adjust for the current timezone. This is an arbitrary date
    // (x-27-2007) and time (10:45 PM).
    RetainPtr<NSCalendar> gregorian = adoptNS([[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]);
    [gregorian.get() setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
    RetainPtr<NSDateComponents> components = adoptNS([[NSDateComponents alloc] init]);
    [components.get() setDay:27];
    [components.get() setYear:2007];
    [components.get() setHour:22];
    [components.get() setMinute:45];

    static const NSUInteger numberOfGregorianMonths = [[dateFormatter monthSymbols] count];
    ASSERT(numberOfGregorianMonths == 12);

    // For each month (in the Gregorian Calendar), format a date and measure its length.
    NSUInteger totalMonthsToTest = 1;
    if (type == DateComponents::Date
        || type == DateComponents::DateTime
        || type == DateComponents::DateTimeLocal
        || type == DateComponents::Month)
        totalMonthsToTest = numberOfGregorianMonths;
    for (NSUInteger i = 0; i < totalMonthsToTest; ++i) {
        [components.get() setMonth:(i + 1)];
        NSDate *date = [gregorian.get() dateFromComponents:components.get()];
        NSString *formattedDate = [dateFormatter stringFromDate:date];
        maximumWidth = max(maximumWidth, measurer.measureText(String(formattedDate)));
    }

    return maximumWidth;
}

} // namespace WebCore

#endif // PLATFORM(IOS_FAMILY)