annotate src/textabbrev.cpp @ 518:5c846f3c9244

Fix inaccurate and frightening text
author Chris Cannam
date Thu, 20 Oct 2011 15:39:46 +0100
parents 6fc4fafc5e6b
children 533519ebc0cb
rev   line source
Chris@50 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@50 2
Chris@50 3 /*
Chris@57 4 EasyMercurial
Chris@57 5
Chris@57 6 Based on HgExplorer by Jari Korhonen
Chris@57 7 Copyright (c) 2010 Jari Korhonen
Chris@244 8 Copyright (c) 2011 Chris Cannam
Chris@244 9 Copyright (c) 2011 Queen Mary, University of London
Chris@50 10
Chris@50 11 This program is free software; you can redistribute it and/or
Chris@50 12 modify it under the terms of the GNU General Public License as
Chris@50 13 published by the Free Software Foundation; either version 2 of the
Chris@50 14 License, or (at your option) any later version. See the file
Chris@50 15 COPYING included with this distribution for more information.
Chris@50 16 */
Chris@50 17
Chris@50 18 #include "textabbrev.h"
Chris@463 19 #include "debug.h"
Chris@50 20
Chris@50 21 #include <QFontMetrics>
Chris@50 22 #include <QApplication>
Chris@50 23
Chris@50 24 #include <iostream>
Chris@50 25
Chris@50 26 QString
Chris@50 27 TextAbbrev::getDefaultEllipsis()
Chris@50 28 {
Chris@50 29 return "...";
Chris@50 30 }
Chris@50 31
Chris@50 32 int
Chris@50 33 TextAbbrev::getFuzzLength(QString ellipsis)
Chris@50 34 {
Chris@50 35 int len = ellipsis.length();
Chris@50 36 if (len < 3) return len + 3;
Chris@50 37 else if (len > 5) return len + 5;
Chris@50 38 else return len * 2;
Chris@50 39 }
Chris@50 40
Chris@50 41 int
Chris@50 42 TextAbbrev::getFuzzWidth(const QFontMetrics &metrics, QString ellipsis)
Chris@50 43 {
Chris@50 44 int width = metrics.width(ellipsis);
Chris@50 45 return width * 2;
Chris@50 46 }
Chris@50 47
Chris@50 48 QString
Chris@50 49 TextAbbrev::abbreviateTo(QString text, int characters, Policy policy,
Chris@50 50 QString ellipsis)
Chris@50 51 {
Chris@50 52 switch (policy) {
Chris@50 53
Chris@50 54 case ElideEnd:
Chris@50 55 case ElideEndAndCommonPrefixes:
Chris@50 56 text = text.left(characters) + ellipsis;
Chris@50 57 break;
Chris@50 58
Chris@50 59 case ElideStart:
Chris@50 60 text = ellipsis + text.right(characters);
Chris@50 61 break;
Chris@50 62
Chris@50 63 case ElideMiddle:
Chris@50 64 if (characters > 2) {
Chris@50 65 text = text.left(characters/2 + 1) + ellipsis
Chris@50 66 + text.right(characters - (characters/2 + 1));
Chris@50 67 } else {
Chris@50 68 text = text.left(characters) + ellipsis;
Chris@50 69 }
Chris@50 70 break;
Chris@50 71 }
Chris@50 72
Chris@50 73 return text;
Chris@50 74 }
Chris@50 75
Chris@50 76 QString
Chris@50 77 TextAbbrev::abbreviate(QString text, int maxLength, Policy policy, bool fuzzy,
Chris@50 78 QString ellipsis)
Chris@50 79 {
Chris@50 80 if (ellipsis == "") ellipsis = getDefaultEllipsis();
Chris@50 81 int fl = (fuzzy ? getFuzzLength(ellipsis) : 0);
Chris@50 82 if (maxLength <= ellipsis.length()) maxLength = ellipsis.length() + 1;
Chris@50 83 if (text.length() <= maxLength + fl) return text;
Chris@50 84
Chris@50 85 int truncated = maxLength - ellipsis.length();
Chris@50 86 return abbreviateTo(text, truncated, policy, ellipsis);
Chris@50 87 }
Chris@50 88
Chris@50 89 QString
Chris@50 90 TextAbbrev::abbreviate(QString text,
Chris@50 91 const QFontMetrics &metrics, int &maxWidth,
Chris@53 92 Policy policy, QString ellipsis, int wrapLines)
Chris@50 93 {
Chris@50 94 if (ellipsis == "") ellipsis = getDefaultEllipsis();
Chris@50 95
Chris@463 96 // std::cerr << "TextAbbrev::abbreviate: text = " << text << std::endl;
Chris@463 97
Chris@50 98 int tw = metrics.width(text);
Chris@463 99 int tl = text.length();
Chris@463 100
Chris@463 101 // std::cerr << "\n*** TextAbbrev::abbreviate: tw = " << tw << "\n***\n" << std::endl;
Chris@50 102
Chris@50 103 if (tw <= maxWidth) {
Chris@50 104 maxWidth = tw;
Chris@50 105 return text;
Chris@50 106 }
Chris@50 107
Chris@50 108 QString original = text;
Chris@50 109
Chris@463 110 int acw = metrics.averageCharWidth();
Chris@463 111
Chris@53 112 if (wrapLines < 2) {
Chris@50 113
Chris@463 114 // std::cerr << "only " << wrapLines << " line(s) remaining, going by character..." << std::endl;
Chris@50 115
Chris@463 116 int truncateTo = tl;
Chris@463 117
Chris@463 118 while (tw > maxWidth && truncateTo > 1) {
Chris@463 119
Chris@464 120 if (tw > maxWidth + acw * (ellipsis.length() + 5)) {
Chris@463 121 float ratio = float(maxWidth) / float(tw);
Chris@463 122 truncateTo = int(truncateTo * ratio * 1.2) + 10;
Chris@463 123 if (truncateTo >= tl) truncateTo = tl - 1;
Chris@463 124 } else {
Chris@463 125 truncateTo--;
Chris@463 126 }
Chris@53 127
Chris@463 128 // std::cerr << "truncating from " << tl << " to " << truncateTo << std::endl;
Chris@463 129
Chris@463 130 if (truncateTo > ellipsis.length()) {
Chris@463 131 text = abbreviateTo(original, truncateTo, policy, ellipsis);
Chris@53 132 } else {
Chris@53 133 break;
Chris@53 134 }
Chris@53 135
Chris@53 136 tw = metrics.width(text);
Chris@463 137 tl = text.length() - ellipsis.length();
Chris@53 138 }
Chris@463 139
Chris@463 140 // std::cerr << "done, final tw = " << tw << std::endl;
Chris@53 141
Chris@464 142 maxWidth = std::max(maxWidth, tw);
Chris@53 143 return text;
Chris@53 144
Chris@53 145 } else {
Chris@53 146
Chris@53 147 QStringList words = text.split(' ', QString::SkipEmptyParts);
Chris@53 148 text = "";
Chris@53 149
Chris@53 150 tw = 0;
Chris@53 151 int i = 0;
Chris@53 152 QString good = "";
Chris@53 153 int lastUsed = 0;
Chris@463 154
Chris@463 155 // std::cerr << "have " << wrapLines << " lines remaining, going by word..." << std::endl;
Chris@463 156
Chris@53 157 while (tw < maxWidth && i < words.size()) {
Chris@53 158 if (text != "") text += " ";
Chris@53 159 text += words[i++];
Chris@53 160 tw = metrics.width(text);
Chris@53 161 if (tw < maxWidth) {
Chris@53 162 good = text;
Chris@53 163 lastUsed = i;
Chris@53 164 }
Chris@53 165 }
Chris@463 166
Chris@463 167 // std::cerr << "done" << std::endl;
Chris@53 168
Chris@53 169 if (tw < maxWidth) {
Chris@53 170 maxWidth = tw;
Chris@53 171 return text;
Chris@50 172 }
Chris@50 173
Chris@53 174 text = good;
Chris@53 175
Chris@53 176 QString remainder;
Chris@53 177 while (lastUsed < words.size()) {
Chris@53 178 if (remainder != "") remainder += " ";
Chris@53 179 remainder += words[lastUsed++];
Chris@53 180 }
Chris@53 181 remainder = abbreviate(remainder, metrics, maxWidth,
Chris@53 182 policy, ellipsis, wrapLines - 1);
Chris@53 183
Chris@53 184 maxWidth = std::max(maxWidth, tw);
Chris@53 185 text = text + '\n' + remainder;
Chris@53 186 return text;
Chris@50 187 }
Chris@50 188 }
Chris@50 189
Chris@50 190 QStringList
Chris@50 191 TextAbbrev::abbreviate(const QStringList &texts, int maxLength,
Chris@50 192 Policy policy, bool fuzzy, QString ellipsis)
Chris@50 193 {
Chris@50 194 if (policy == ElideEndAndCommonPrefixes &&
Chris@50 195 texts.size() > 1) {
Chris@50 196
Chris@50 197 if (ellipsis == "") ellipsis = getDefaultEllipsis();
Chris@50 198 int fl = (fuzzy ? getFuzzLength(ellipsis) : 0);
Chris@50 199 if (maxLength <= ellipsis.length()) maxLength = ellipsis.length() + 1;
Chris@50 200
Chris@50 201 int maxOrigLength = 0;
Chris@50 202 for (int i = 0; i < texts.size(); ++i) {
Chris@50 203 int len = texts[i].length();
Chris@50 204 if (len > maxOrigLength) maxOrigLength = len;
Chris@50 205 }
Chris@50 206 if (maxOrigLength <= maxLength + fl) return texts;
Chris@50 207
Chris@50 208 return abbreviate(elidePrefixes
Chris@50 209 (texts, maxOrigLength - maxLength, ellipsis),
Chris@50 210 maxLength, ElideEnd, fuzzy, ellipsis);
Chris@50 211 }
Chris@50 212
Chris@50 213 QStringList results;
Chris@50 214 for (int i = 0; i < texts.size(); ++i) {
Chris@50 215 results.push_back
Chris@50 216 (abbreviate(texts[i], maxLength, policy, fuzzy, ellipsis));
Chris@50 217 }
Chris@50 218 return results;
Chris@50 219 }
Chris@50 220
Chris@50 221 QStringList
Chris@50 222 TextAbbrev::abbreviate(const QStringList &texts, const QFontMetrics &metrics,
Chris@50 223 int &maxWidth, Policy policy, QString ellipsis)
Chris@50 224 {
Chris@50 225 if (policy == ElideEndAndCommonPrefixes &&
Chris@50 226 texts.size() > 1) {
Chris@50 227
Chris@50 228 if (ellipsis == "") ellipsis = getDefaultEllipsis();
Chris@50 229
Chris@50 230 int maxOrigWidth = 0;
Chris@50 231 for (int i = 0; i < texts.size(); ++i) {
Chris@50 232 int w = metrics.width(texts[i]);
Chris@50 233 if (w > maxOrigWidth) maxOrigWidth = w;
Chris@50 234 }
Chris@50 235
Chris@50 236 return abbreviate(elidePrefixes(texts, metrics,
Chris@50 237 maxOrigWidth - maxWidth, ellipsis),
Chris@50 238 metrics, maxWidth, ElideEnd, ellipsis);
Chris@50 239 }
Chris@50 240
Chris@50 241 QStringList results;
Chris@50 242 int maxAbbrWidth = 0;
Chris@50 243 for (int i = 0; i < texts.size(); ++i) {
Chris@50 244 int width = maxWidth;
Chris@50 245 QString abbr = abbreviate(texts[i], metrics, width, policy, ellipsis);
Chris@50 246 if (width > maxAbbrWidth) maxAbbrWidth = width;
Chris@50 247 results.push_back(abbr);
Chris@50 248 }
Chris@50 249 maxWidth = maxAbbrWidth;
Chris@50 250 return results;
Chris@50 251 }
Chris@50 252
Chris@50 253 QStringList
Chris@50 254 TextAbbrev::elidePrefixes(const QStringList &texts,
Chris@50 255 int targetReduction,
Chris@50 256 QString ellipsis)
Chris@50 257 {
Chris@50 258 if (texts.empty()) return texts;
Chris@50 259 int plen = getPrefixLength(texts);
Chris@50 260 int fl = getFuzzLength(ellipsis);
Chris@50 261 if (plen < fl) return texts;
Chris@50 262
Chris@50 263 QString prefix = texts[0].left(plen);
Chris@50 264 int truncated = plen;
Chris@50 265 if (plen >= targetReduction + fl) {
Chris@50 266 truncated = plen - targetReduction;
Chris@50 267 } else {
Chris@50 268 truncated = fl;
Chris@50 269 }
Chris@50 270 prefix = abbreviate(prefix, truncated, ElideEnd, false, ellipsis);
Chris@50 271
Chris@50 272 QStringList results;
Chris@50 273 for (int i = 0; i < texts.size(); ++i) {
Chris@50 274 results.push_back
Chris@50 275 (prefix + texts[i].right(texts[i].length() - plen));
Chris@50 276 }
Chris@50 277 return results;
Chris@50 278 }
Chris@50 279
Chris@50 280 QStringList
Chris@50 281 TextAbbrev::elidePrefixes(const QStringList &texts,
Chris@50 282 const QFontMetrics &metrics,
Chris@50 283 int targetWidthReduction,
Chris@50 284 QString ellipsis)
Chris@50 285 {
Chris@50 286 if (texts.empty()) return texts;
Chris@50 287 int plen = getPrefixLength(texts);
Chris@50 288 int fl = getFuzzLength(ellipsis);
Chris@50 289 if (plen < fl) return texts;
Chris@50 290
Chris@50 291 QString prefix = texts[0].left(plen);
Chris@50 292 int pwid = metrics.width(prefix);
Chris@50 293 int twid = pwid - targetWidthReduction;
Chris@50 294 if (twid < metrics.width(ellipsis) * 2) twid = metrics.width(ellipsis) * 2;
Chris@50 295 prefix = abbreviate(prefix, metrics, twid, ElideEnd, ellipsis);
Chris@50 296
Chris@50 297 QStringList results;
Chris@50 298 for (int i = 0; i < texts.size(); ++i) {
Chris@50 299 results.push_back
Chris@50 300 (prefix + texts[i].right(texts[i].length() - plen));
Chris@50 301 }
Chris@50 302 return results;
Chris@50 303 }
Chris@50 304
Chris@50 305 static bool
Chris@50 306 havePrefix(QString prefix, const QStringList &texts)
Chris@50 307 {
Chris@50 308 for (int i = 1; i < texts.size(); ++i) {
Chris@50 309 if (!texts[i].startsWith(prefix)) return false;
Chris@50 310 }
Chris@50 311 return true;
Chris@50 312 }
Chris@50 313
Chris@50 314 int
Chris@50 315 TextAbbrev::getPrefixLength(const QStringList &texts)
Chris@50 316 {
Chris@50 317 QString reference = texts[0];
Chris@50 318
Chris@50 319 if (reference == "" || havePrefix(reference, texts)) {
Chris@50 320 return reference.length();
Chris@50 321 }
Chris@50 322
Chris@50 323 int candidate = reference.length();
Chris@50 324 QString splitChars(";:,./#-!()$_+=[]{}\\");
Chris@50 325
Chris@50 326 while (--candidate > 1) {
Chris@50 327 if (splitChars.contains(reference[candidate])) {
Chris@50 328 if (havePrefix(reference.left(candidate), texts)) {
Chris@50 329 break;
Chris@50 330 }
Chris@50 331 }
Chris@50 332 }
Chris@50 333
Chris@50 334 // std::cerr << "TextAbbrev::getPrefixLength: prefix length is " << candidate << std::endl;
Chris@50 335 // for (int i = 0; i < texts.size(); ++i) {
Chris@50 336 // std::cerr << texts[i].left(candidate).toStdString() << "|" << texts[i].right(texts[i].length() - candidate).toStdString() << std::endl;
Chris@50 337 // }
Chris@50 338
Chris@50 339 return candidate;
Chris@50 340 }
Chris@50 341