Chris@376: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
Chris@376: 
Chris@376: /*
Chris@376:     Sonic Visualiser
Chris@376:     An audio file viewer and annotation editor.
Chris@376:     Centre for Digital Music, Queen Mary, University of London.
Chris@376:     This file copyright 2006-2007 Chris Cannam and QMUL.
Chris@376:     
Chris@376:     This program is free software; you can redistribute it and/or
Chris@376:     modify it under the terms of the GNU General Public License as
Chris@376:     published by the Free Software Foundation; either version 2 of the
Chris@376:     License, or (at your option) any later version.  See the file
Chris@376:     COPYING included with this distribution for more information.
Chris@376: */
Chris@376: 
Chris@376: #include "TextAbbrev.h"
Chris@376: 
Chris@376: #include <QFontMetrics>
Chris@376: #include <QApplication>
Chris@376: 
Chris@376: #include <iostream>
Chris@376: 
Chris@376: QString
Chris@376: TextAbbrev::getDefaultEllipsis()
Chris@376: {
Chris@376:     return "...";
Chris@376: }
Chris@376: 
Chris@376: int
Chris@376: TextAbbrev::getFuzzLength(QString ellipsis)
Chris@376: {
Chris@376:     int len = ellipsis.length();
Chris@376:     if (len < 3) return len + 3;
Chris@376:     else if (len > 5) return len + 5;
Chris@376:     else return len * 2;
Chris@376: }
Chris@376: 
Chris@376: int
Chris@376: TextAbbrev::getFuzzWidth(const QFontMetrics &metrics, QString ellipsis)
Chris@376: {
Chris@376:     int width = metrics.width(ellipsis);
Chris@376:     return width * 2;
Chris@376: }
Chris@376: 
Chris@376: QString
Chris@376: TextAbbrev::abbreviateTo(QString text, int characters, Policy policy,
Chris@376:                          QString ellipsis)
Chris@376: {
Chris@376:     switch (policy) {
Chris@376: 
Chris@376:     case ElideEnd:
Chris@376:     case ElideEndAndCommonPrefixes:
Chris@376:         text = text.left(characters) + ellipsis;
Chris@376:         break;
Chris@376:         
Chris@376:     case ElideStart:
Chris@376:         text = ellipsis + text.right(characters);
Chris@376:         break;
Chris@376: 
Chris@376:     case ElideMiddle:
Chris@376:         if (characters > 2) {
Chris@376:             text = text.left(characters/2 + 1) + ellipsis
Chris@376:                 + text.right(characters - (characters/2 + 1));
Chris@376:         } else {
Chris@376:             text = text.left(characters) + ellipsis;
Chris@376:         }
Chris@376:         break;
Chris@376:     }
Chris@376: 
Chris@376:     return text;
Chris@376: }
Chris@376: 
Chris@376: QString
Chris@376: TextAbbrev::abbreviate(QString text, int maxLength, Policy policy, bool fuzzy,
Chris@376:                        QString ellipsis)
Chris@376: {
Chris@376:     if (ellipsis == "") ellipsis = getDefaultEllipsis();
Chris@376:     int fl = (fuzzy ? getFuzzLength(ellipsis) : 0);
Chris@376:     if (maxLength <= ellipsis.length()) maxLength = ellipsis.length() + 1;
Chris@376:     if (text.length() <= maxLength + fl) return text;
Chris@376: 
Chris@376:     int truncated = maxLength - ellipsis.length();
Chris@376:     return abbreviateTo(text, truncated, policy, ellipsis);
Chris@376: }
Chris@376: 
Chris@376: QString
Chris@376: TextAbbrev::abbreviate(QString text,
Chris@376:                        const QFontMetrics &metrics, int &maxWidth,
Chris@376:                        Policy policy, QString ellipsis)
Chris@376: {
Chris@376:     if (ellipsis == "") ellipsis = getDefaultEllipsis();
Chris@376: 
Chris@376:     int tw = metrics.width(text);
Chris@376: 
Chris@376:     if (tw <= maxWidth) {
Chris@376:         maxWidth = tw;
Chris@376:         return text;
Chris@376:     }
Chris@376: 
Chris@376:     int truncated = text.length();
Chris@376:     QString original = text;
Chris@376: 
Chris@376:     while (tw > maxWidth && truncated > 1) {
Chris@376: 
Chris@376:         truncated--;
Chris@376: 
Chris@376:         if (truncated > ellipsis.length()) {
Chris@376:             text = abbreviateTo(original, truncated, policy, ellipsis);
Chris@376:         } else {
Chris@376:             break;
Chris@376:         }
Chris@376: 
Chris@376:         tw = metrics.width(text);
Chris@376:     }
Chris@376: 
Chris@376:     maxWidth = tw;
Chris@376:     return text;
Chris@376: }
Chris@376: 
Chris@376: QStringList
Chris@376: TextAbbrev::abbreviate(const QStringList &texts, int maxLength,
Chris@376:                        Policy policy, bool fuzzy, QString ellipsis)
Chris@376: {
Chris@376:     if (policy == ElideEndAndCommonPrefixes &&
Chris@376:         texts.size() > 1) {
Chris@376: 
Chris@376:         if (ellipsis == "") ellipsis = getDefaultEllipsis();
Chris@376:         int fl = (fuzzy ? getFuzzLength(ellipsis) : 0);
Chris@376:         if (maxLength <= ellipsis.length()) maxLength = ellipsis.length() + 1;
Chris@376: 
Chris@376:         int maxOrigLength = 0;
Chris@376:         for (int i = 0; i < texts.size(); ++i) {
Chris@376:             int len = texts[i].length();
Chris@376:             if (len > maxOrigLength) maxOrigLength = len;
Chris@376:         }
Chris@376:         if (maxOrigLength <= maxLength + fl) return texts;
Chris@376: 
Chris@376:         return abbreviate(elidePrefixes
Chris@376:                           (texts, maxOrigLength - maxLength, ellipsis),
Chris@376:                           maxLength, ElideEnd, fuzzy, ellipsis);
Chris@376:     }
Chris@376: 
Chris@376:     QStringList results;
Chris@376:     for (int i = 0; i < texts.size(); ++i) {
Chris@376:         results.push_back
Chris@376:             (abbreviate(texts[i], maxLength, policy, fuzzy, ellipsis));
Chris@376:     }
Chris@376:     return results;
Chris@376: }
Chris@376: 
Chris@376: QStringList
Chris@376: TextAbbrev::abbreviate(const QStringList &texts, const QFontMetrics &metrics,
Chris@376:                        int &maxWidth, Policy policy, QString ellipsis)
Chris@376: {
Chris@376:     if (policy == ElideEndAndCommonPrefixes &&
Chris@376:         texts.size() > 1) {
Chris@376: 
Chris@376:         if (ellipsis == "") ellipsis = getDefaultEllipsis();
Chris@376: 
Chris@376:         int maxOrigWidth = 0;
Chris@376:         for (int i = 0; i < texts.size(); ++i) {
Chris@376:             int w = metrics.width(texts[i]);
Chris@376:             if (w > maxOrigWidth) maxOrigWidth = w;
Chris@376:         }
Chris@376: 
Chris@376:         return abbreviate(elidePrefixes(texts, metrics,
Chris@376:                                         maxOrigWidth - maxWidth, ellipsis),
Chris@376:                           metrics, maxWidth, ElideEnd, ellipsis);
Chris@376:     }
Chris@376: 
Chris@376:     QStringList results;
Chris@376:     int maxAbbrWidth = 0;
Chris@376:     for (int i = 0; i < texts.size(); ++i) {
Chris@376:         int width = maxWidth;
Chris@376:         QString abbr = abbreviate(texts[i], metrics, width, policy, ellipsis);
Chris@376:         if (width > maxAbbrWidth) maxAbbrWidth = width;
Chris@376:         results.push_back(abbr);
Chris@376:     }
Chris@376:     maxWidth = maxAbbrWidth;
Chris@376:     return results;
Chris@376: }
Chris@376: 
Chris@376: QStringList
Chris@376: TextAbbrev::elidePrefixes(const QStringList &texts,
Chris@376:                           int targetReduction,
Chris@376:                           QString ellipsis)
Chris@376: {
Chris@376:     if (texts.empty()) return texts;
Chris@376:     int plen = getPrefixLength(texts);
Chris@376:     int fl = getFuzzLength(ellipsis);
Chris@376:     if (plen < fl) return texts;
Chris@376: 
Chris@376:     QString prefix = texts[0].left(plen);
Chris@376:     int truncated = plen;
Chris@376:     if (plen >= targetReduction + fl) {
Chris@376:         truncated = plen - targetReduction;
Chris@376:     } else {
Chris@376:         truncated = fl;
Chris@376:     }
Chris@376:     prefix = abbreviate(prefix, truncated, ElideEnd, false, ellipsis);
Chris@376: 
Chris@376:     QStringList results;
Chris@376:     for (int i = 0; i < texts.size(); ++i) {
Chris@376:         results.push_back
Chris@376:             (prefix + texts[i].right(texts[i].length() - plen));
Chris@376:     }
Chris@376:     return results;
Chris@376: }
Chris@376: 
Chris@376: QStringList
Chris@376: TextAbbrev::elidePrefixes(const QStringList &texts,
Chris@376:                           const QFontMetrics &metrics,
Chris@376:                           int targetWidthReduction,
Chris@376:                           QString ellipsis)
Chris@376: {
Chris@376:     if (texts.empty()) return texts;
Chris@376:     int plen = getPrefixLength(texts);
Chris@376:     int fl = getFuzzLength(ellipsis);
Chris@376:     if (plen < fl) return texts;
Chris@376: 
Chris@376:     QString prefix = texts[0].left(plen);
Chris@376:     int pwid = metrics.width(prefix);
Chris@376:     int twid = pwid - targetWidthReduction;
Chris@376:     if (twid < metrics.width(ellipsis) * 2) twid = metrics.width(ellipsis) * 2;
Chris@376:     prefix = abbreviate(prefix, metrics, twid, ElideEnd, ellipsis);
Chris@376: 
Chris@376:     QStringList results;
Chris@376:     for (int i = 0; i < texts.size(); ++i) {
Chris@376:         results.push_back
Chris@376:             (prefix + texts[i].right(texts[i].length() - plen));
Chris@376:     }
Chris@376:     return results;
Chris@376: }
Chris@376: 
Chris@376: static bool
Chris@376: havePrefix(QString prefix, const QStringList &texts)
Chris@376: {
Chris@376:     for (int i = 1; i < texts.size(); ++i) {
Chris@376:         if (!texts[i].startsWith(prefix)) return false;
Chris@376:     }
Chris@376:     return true;
Chris@376: }
Chris@376: 
Chris@376: int
Chris@376: TextAbbrev::getPrefixLength(const QStringList &texts)
Chris@376: {
Chris@376:     QString reference = texts[0];
Chris@376: 
Chris@376:     if (reference == "" || havePrefix(reference, texts)) {
Chris@376:         return reference.length();
Chris@376:     }
Chris@376: 
Chris@376:     int candidate = reference.length();
Chris@376:     QString splitChars(";:,./#-!()$_+=[]{}\\");
Chris@376: 
Chris@376:     while (--candidate > 1) {
Chris@376:         if (splitChars.contains(reference[candidate])) {
Chris@376:             if (havePrefix(reference.left(candidate), texts)) {
Chris@376:                 break;
Chris@376:             }
Chris@376:         }
Chris@376:     }
Chris@376: 
Chris@587: //    SVDEBUG << "TextAbbrev::getPrefixLength: prefix length is " << candidate << endl;
Chris@376: //    for (int i = 0; i < texts.size(); ++i) {
Chris@682: //        cerr << texts[i].left(candidate) << "|" << texts[i].right(texts[i].length() - candidate) << endl;
Chris@376: //    }
Chris@376: 
Chris@376:     return candidate;
Chris@376: }
Chris@376: