1 /*****************************************************************************
2  * Copyright (C) 2003-2004 Max Howell <max.howell@methylblue.com>            *
3  * Copyright (C) 2004-2019 Krusader Krew [https://krusader.org]              *
4  *                                                                           *
5  * This file is part of Krusader [https://krusader.org].                     *
6  *                                                                           *
7  * Krusader is free software: you can redistribute it and/or modify          *
8  * it under the terms of the GNU General Public License as published by      *
9  * the Free Software Foundation, either version 2 of the License, or         *
10  * (at your option) any later version.                                       *
11  *                                                                           *
12  * Krusader is distributed in the hope that it will be useful,               *
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of            *
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             *
15  * GNU General Public License for more details.                              *
16  *                                                                           *
17  * You should have received a copy of the GNU General Public License         *
18  * along with Krusader.  If not, see [http://www.gnu.org/licenses/].         *
19  *****************************************************************************/
20 
21 // QtCore
22 #include <QList>
23 // QtGui
24 #include <QFont>
25 #include <QFontMetrics>
26 #include <QPainter>
27 
28 #include <KCoreAddons/KStringHandler>
29 
30 #include "Config.h"
31 #include "fileTree.h"
32 #include "radialMap.h"
33 #include "sincos.h"
34 #include "widget.h"
35 
36 namespace RadialMap
37 {
38 struct Label {
LabelRadialMap::Label39     Label(const RadialMap::Segment *s, int l) : segment(s), lvl(l), a(segment->start() + (segment->length() / 2)) { }
40 
tooCloseRadialMap::Label41     bool tooClose(const int &aa) const {
42         return (a > aa - LABEL_ANGLE_MARGIN && a < aa + LABEL_ANGLE_MARGIN);
43     }
44 
45     const RadialMap::Segment *segment;
46     const unsigned int lvl;
47     const int a;
48 
49     int x1, y1, x2, y2, x3;
50     int tx, ty;
51 
52     QString qs;
53 };
54 
55 class LabelList : public QList<Label *>
56 {
57 
58 public:
~LabelList()59     ~LabelList() {
60         QListIterator<Label *> it(*this);
61         while (it.hasNext())
62             delete it.next();
63     }
64 
inSort(Label * label1)65     void inSort(Label * label1) {
66         for (QList<Label *>::iterator it(begin()); it != end(); ++it) {
67             Label * label2 = *it;
68             bool ins = false;
69 
70             //you add 1440 to work round the fact that later you want the circle split vertically
71             //and as it is you start at 3 o' clock. It's to do with rightPrevY, stops annoying bug
72 
73             int a1 = label1->a + 1440;
74             int a2 = label2->a + 1440;
75 
76             if (a1 == a2)
77                 ins = true;
78 
79             if (a1 > 5760) a1 -= 5760;
80             if (a2 > 5760) a2 -= 5760;
81 
82             if (a1 < a2)
83                 ins = true;
84 
85             if (ins) {
86                 insert(it, label1);
87                 return;
88             }
89         }
90 
91         append(label1);
92     }
93 };
94 }
95 
96 
97 void
paintExplodedLabels(QPainter & paint) const98 RadialMap::Widget::paintExplodedLabels(QPainter &paint) const
99 {
100     //we are a friend of RadialMap::Map
101 
102     LabelList list;
103     unsigned int startLevel = 0;
104 
105 
106     //1. Create list of labels  sorted in the order they will be rendered
107 
108     if (m_focus != NULL && m_focus->file() != m_tree) { //separate behavior for selected vs unselected segments
109         //don't bother with files
110         if (m_focus->file() == 0 || !m_focus->file()->isDir()) return;
111 
112         //find the range of levels we will be potentially drawing labels for
113         for (const Directory *p = (const Directory *)m_focus->file();
114                 p != m_tree;
115                 ++startLevel) { //startLevel is the level above whatever m_focus is in
116             p = p->parent();
117         }
118 
119         //range=2 means 2 levels to draw labels for
120 
121         unsigned int a1, a2, minAngle;
122 
123         a1 = m_focus->start();
124         a2 = m_focus->end();  // boundary angles
125         minAngle = int(m_focus->length() * LABEL_MIN_ANGLE_FACTOR);
126 
127 
128 #define segment (*it)
129 #define ring (m_map.m_signature + i)
130 
131         //**** Levels should be on a scale starting with 0
132         //**** range is a useless parameter
133         //**** keep a topblock var which is the lowestLevel OR startLevel for identation purposes
134         for (unsigned int i = startLevel; i <= m_map.m_visibleDepth; ++i) {
135             for (Iterator<Segment> it = ring->iterator(); it != ring->end(); ++it)
136                 if (segment->start() >= a1 && segment->end() <= a2)
137                     if (segment->length() > minAngle)
138                         list.inSort(new Label(segment, i));
139         }
140 
141 #undef ring
142 #undef segment
143 
144     } else {
145 
146 #define ring m_map.m_signature
147 
148         for (Iterator<Segment> it = ring->iterator(); it != ring->end(); ++it)
149             if ((*it)->length() > 288)
150                 list.inSort(new Label((*it), 0));
151 
152 #undef ring
153 
154     }
155 
156     //2. Check to see if any adjacent labels are too close together
157     //   if so, remove the least significant labels
158 
159     int itn = 0;
160     int jtn = 1;
161 
162     while (jtn < list.count()) {
163         //this method is fairly efficient
164 
165         if (list[ itn ]->tooClose(list[ jtn ]->a)) {
166             if (list[ itn ]->lvl > list[ jtn ]->lvl) {
167                 delete list[ itn ];
168                 list.removeAt(itn);
169                 jtn--;
170                 itn = jtn;
171             } else {
172                 delete list[ jtn ];
173                 list.removeAt(jtn);
174             }
175         } else
176             ++itn;
177 
178         jtn = itn;
179         ++jtn;
180     }
181 
182     LabelList::iterator it = list.begin();
183 
184     //used in next two steps
185     bool varySizes;
186     //**** should perhaps use doubles
187     int  *sizes = new int [ m_map.m_visibleDepth + 1 ]; //**** make sizes an array of floats I think instead (or doubles)
188 
189     do {
190         //3. Calculate font sizes
191 
192         {
193             //determine current range of levels to draw for
194             unsigned int range = 0;
195 
196             for (it = list.begin(); it != list.end(); ++it) {
197                 unsigned int lvl = (*it)->lvl;
198                 if (lvl > range)
199                     range = lvl;
200 
201                 //**** better way would just be to assign if nothing is range
202             }
203 
204             range -= startLevel; //range 0 means 1 level of labels
205 
206             varySizes = Config::varyLabelFontSizes && (range != 0);
207 
208             if (varySizes) {
209                 //create an array of font sizes for various levels
210                 //will exceed normal font pitch automatically if necessary, but not minPitch
211                 //**** this needs to be checked lots
212 
213                 //**** what if this is negative (min size gtr than default size)
214                 unsigned int step = (paint.font().pointSize() - Config::minFontPitch) / range;
215                 if (step == 0) step = 1;
216 
217                 for (unsigned int x = range + startLevel, y = Config::minFontPitch;
218                         x >= startLevel;
219                         y += step, --x) {
220                     sizes[x] = y;
221                 }
222             }
223         }
224 
225         //4. determine label co-ordinates
226 
227         int x1, y1, x2, y2, x3, tx, ty; //coords
228         double sinra, cosra, ra;  //angles
229 
230         int cx = m_map.width()  / 2 + m_offset.x();  //centre relative to canvas
231         int cy = m_map.height() / 2 + m_offset.y();
232 
233         int spacer, preSpacer = int(m_map.m_ringBreadth * 0.5) + m_map.m_innerRadius;
234         int fullStrutLength = (m_map.width() - m_map.MAP_2MARGIN) / 2 + LABEL_MAP_SPACER;   //full length of a strut from map center
235 
236         int prevLeftY  = 0;
237         int prevRightY = height();
238 
239         bool rightSide;
240 
241         QFont font;
242 
243         for (it = list.begin(); it != list.end(); ++it) {
244             //** bear in mind that text is drawn with QPoint param as BOTTOM left corner of text box
245             if (varySizes) font.setPointSize(sizes[(*it)->lvl]);
246             QFontMetrics fm(font);
247             int fmh  = fm.height(); //used to ensure label texts don't overlap
248             int fmhD4 = fmh / 4;
249 
250             fmh += LABEL_TEXT_VMARGIN;
251 
252             rightSide = ((*it)->a < 1440 || (*it)->a > 4320);
253 
254             ra = M_PI / 2880 * (*it)->a; //convert to radians
255 #if 0
256             sincos(ra, &sinra, &cosra);
257 #endif
258             sinra = sin(ra); cosra = cos(ra);
259 
260             spacer = preSpacer + m_map.m_ringBreadth * (*it)->lvl;
261 
262             x1 = cx + (int)(cosra * spacer);
263             y1 = cy - (int)(sinra * spacer);
264             y2 = y1 - (int)(sinra * (fullStrutLength - spacer));
265 
266             if (rightSide) {  //righthand side, going upwards
267 
268                 if (y2 > prevRightY /*- fmh*/)  //then it is too low, needs to be drawn higher
269                     y2 = prevRightY /*- fmh*/;
270 
271             } else { //lefthand side, going downwards
272 
273                 if (y2 < prevLeftY/* + fmh*/)  //then we're too high, need to be drawn lower
274                     y2 = prevLeftY /*+ fmh*/;
275             }
276 
277             x2 = x1 - int(double(y2 - y1) / tan(ra));
278             ty = y2 + fmhD4;
279 
280             QString qs;
281             if (rightSide) {
282 
283                 if (x2 > width() || ty < fmh || x2 < x1) {
284                     //skip this strut
285                     //**** don't duplicate this code
286                     delete *it;
287                     it = list.erase(it);   //will delete the label and set it to list.current() which _should_ be the next ptr
288                     break;
289                 }
290 
291                 prevRightY = ty - fmh - fmhD4; //must be after above's "continue"
292 
293                 qs = fm.elidedText((*it)->segment->file()->name(), Qt::ElideMiddle, width() - x2);
294 
295                 x3 = width() - fm.width(qs)
296                      - LABEL_HMARGIN //outer margin
297                      - LABEL_TEXT_HMARGIN //margin between strut and text
298                      //- ((*it)->lvl - startLevel) * LABEL_HMARGIN //indentation
299                      ;
300                 if (x3 < x2) x3 = x2;
301                 tx = x3 + LABEL_TEXT_HMARGIN;
302 
303             } else {
304 
305                 if (x2 < 0 || ty > height() || x2 > x1) {
306                     //skip this strut
307                     delete *it;
308                     it = list.erase(it);   //will delete the label and set it to list.current() which _should_ be the next ptr
309                     break;
310                 }
311 
312                 prevLeftY = ty + fmh - fmhD4;
313 
314                 qs = fm.elidedText((*it)->segment->file()->name(), Qt::ElideMiddle, x2);
315 
316                 //**** needs a little tweaking:
317 
318                 tx = fm.width(qs) + LABEL_HMARGIN/* + ((*it)->lvl - startLevel) * LABEL_HMARGIN*/;
319                 if (tx > x2) {  //text is too long
320                     tx = LABEL_HMARGIN + x2 - tx; //some text will be lost from sight
321                     x3 = x2; //no text margin (right side of text here)
322                 } else {
323                     x3 = tx + LABEL_TEXT_HMARGIN;
324                     tx = LABEL_HMARGIN /* + ((*it)->lvl - startLevel) * LABEL_HMARGIN*/;
325                 }
326             }
327 
328             (*it)->x1 = x1;
329             (*it)->y1 = y1;
330             (*it)->x2 = x2;
331             (*it)->y2 = y2;
332             (*it)->x3 = x3;
333             (*it)->tx = tx;
334             (*it)->ty = ty;
335             (*it)->qs = qs;
336         }
337 
338         //if an element is deleted at this stage, we need to do this whole
339         //iteration again, thus the following loop
340         //**** in rare case that deleted label was last label in top level
341         //     and last in labelList too, this will not work as expected (not critical)
342 
343     } while (it != list.end());
344 
345 
346     //5. Render labels
347 
348     paint.setPen(QPen(Qt::black, 1));
349 
350     for (it = list.begin(); it != list.end(); ++it) {
351         if (varySizes) {
352             //**** how much overhead in making new QFont each time?
353             //     (implicate sharing remember)
354             QFont font = paint.font();
355             font.setPointSize(sizes[(*it)->lvl]);
356             paint.setFont(font);
357         }
358 
359         paint.drawEllipse((*it)->x1 - 3, (*it)->y1 - 3, 7, 7);   //**** CPU intensive! better to use a pixmap
360         paint.drawLine((*it)->x1, (*it)->y1, (*it)->x2, (*it)->y2);
361         paint.drawLine((*it)->x2, (*it)->y2, (*it)->x3, (*it)->y2);
362         paint.drawText((*it)->tx, (*it)->ty, (*it)->qs);
363     }
364 
365     delete [] sizes;
366 }
367