]> git.lyx.org Git - lyx.git/blob - src/insets/InsetCollapsable.cpp
Change tracking cue: paint over labels in text and prepare for further work
[lyx.git] / src / insets / InsetCollapsable.cpp
1 /**
2  * \file InsetCollapsable.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Alejandro Aguilar Sierra
7  * \author Jürgen Vigna
8  * \author Lars Gullik Bjønnes
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "InsetCollapsable.h"
16
17 #include "Buffer.h"
18 #include "BufferView.h"
19 #include "Cursor.h"
20 #include "Dimension.h"
21 #include "FuncRequest.h"
22 #include "FuncStatus.h"
23 #include "InsetLayout.h"
24 #include "Lexer.h"
25 #include "MetricsInfo.h"
26 #include "OutputParams.h"
27
28 #include "frontends/FontMetrics.h"
29 #include "frontends/Painter.h"
30
31 #include "support/debug.h"
32 #include "support/docstream.h"
33 #include "support/gettext.h"
34 #include "support/lassert.h"
35 #include "support/lstrings.h"
36
37 using namespace std;
38
39
40 namespace lyx {
41
42 InsetCollapsable::InsetCollapsable(Buffer * buf, InsetText::UsePlain ltype)
43         : InsetText(buf, ltype), status_(Open)
44 {
45         setDrawFrame(true);
46         setFrameColor(Color_collapsableframe);
47 }
48
49
50 // The sole purpose of this copy constructor is to make sure
51 // that the view_ map is not copied and remains empty.
52 InsetCollapsable::InsetCollapsable(InsetCollapsable const & rhs)
53         : InsetText(rhs),
54           status_(rhs.status_),
55           labelstring_(rhs.labelstring_)
56 {}
57
58
59 InsetCollapsable::~InsetCollapsable()
60 {
61         map<BufferView const *, View>::iterator it = view_.begin();
62         map<BufferView const *, View>::iterator end = view_.end();
63         for (; it != end; ++it)
64                 if (it->second.mouse_hover_)
65                         it->first->clearLastInset(this);
66 }
67
68
69 InsetCollapsable::CollapseStatus InsetCollapsable::status(BufferView const & bv) const
70 {
71         if (decoration() == InsetLayout::CONGLOMERATE)
72                 return status_;
73         return view_[&bv].auto_open_ ? Open : status_;
74 }
75
76
77 InsetCollapsable::Geometry InsetCollapsable::geometry(BufferView const & bv) const
78 {
79         switch (decoration()) {
80         case InsetLayout::CLASSIC:
81                 if (status(bv) == Open)
82                         return view_[&bv].openinlined_ ? LeftButton : TopButton;
83                 return ButtonOnly;
84
85         case InsetLayout::MINIMALISTIC:
86                 return status(bv) == Open ? NoButton : ButtonOnly ;
87
88         case InsetLayout::CONGLOMERATE:
89                 return status(bv) == Open ? SubLabel : Corners ;
90
91         case InsetLayout::DEFAULT:
92                 break; // this shouldn't happen
93         }
94
95         // dummy return value to shut down a warning,
96         // this is dead code.
97         return NoButton;
98 }
99
100
101 docstring InsetCollapsable::toolTip(BufferView const & bv, int x, int y) const
102 {
103         Dimension const dim = dimensionCollapsed(bv);
104         if (geometry(bv) == NoButton)
105                 return translateIfPossible(getLayout().labelstring());
106         if (x > xo(bv) + dim.wid || y > yo(bv) + dim.des || isOpen(bv))
107                 return docstring();
108
109         return toolTipText();
110 }
111
112
113 void InsetCollapsable::write(ostream & os) const
114 {
115         os << "status ";
116         switch (status_) {
117         case Open:
118                 os << "open";
119                 break;
120         case Collapsed:
121                 os << "collapsed";
122                 break;
123         }
124         os << "\n";
125         text().write(os);
126 }
127
128
129 void InsetCollapsable::read(Lexer & lex)
130 {
131         lex.setContext("InsetCollapsable::read");
132         string tmp_token;
133         status_ = Collapsed;
134         lex >> "status" >> tmp_token;
135         if (tmp_token == "open")
136                 status_ = Open;
137
138         InsetText::read(lex);
139         setButtonLabel();
140 }
141
142
143 Dimension InsetCollapsable::dimensionCollapsed(BufferView const & bv) const
144 {
145         Dimension dim;
146         FontInfo labelfont(getLabelfont());
147         labelfont.realize(sane_font);
148         theFontMetrics(labelfont).buttonText(
149                 buttonLabel(bv), dim.wid, dim.asc, dim.des);
150         return dim;
151 }
152
153
154 void InsetCollapsable::metrics(MetricsInfo & mi, Dimension & dim) const
155 {
156         view_[mi.base.bv].auto_open_ = mi.base.bv->cursor().isInside(this);
157
158         FontInfo tmpfont = mi.base.font;
159         mi.base.font = getFont();
160         mi.base.font.realize(tmpfont);
161
162         BufferView const & bv = *mi.base.bv;
163
164         switch (geometry(bv)) {
165         case NoButton:
166                 InsetText::metrics(mi, dim);
167                 break;
168         case Corners:
169                 InsetText::metrics(mi, dim);
170                 dim.des -= 3;
171                 dim.asc -= 1;
172                 break;
173         case SubLabel: {
174                 InsetText::metrics(mi, dim);
175                 // consider width of the inset label
176                 FontInfo font(getLabelfont());
177                 font.realize(sane_font);
178                 font.decSize();
179                 font.decSize();
180                 int w = 0;
181                 int a = 0;
182                 int d = 0;
183                 theFontMetrics(font).rectText(buttonLabel(bv), w, a, d);
184                 dim.des += a + d;
185                 break;
186                 }
187         case TopButton:
188         case LeftButton:
189         case ButtonOnly:
190                 if (hasFixedWidth()){
191                         int const mindim = view_[&bv].button_dim_.x2 - view_[&bv].button_dim_.x1;
192                         if (mi.base.textwidth < mindim)
193                                 mi.base.textwidth = mindim;
194                 }
195                 dim = dimensionCollapsed(bv);
196                 if (geometry(bv) == TopButton || geometry(bv) == LeftButton) {
197                         Dimension textdim;
198                         InsetText::metrics(mi, textdim);
199                         view_[&bv].openinlined_ = (textdim.wid + dim.wid) < mi.base.textwidth;
200                         if (view_[&bv].openinlined_) {
201                                 // Correct for button width.
202                                 dim.wid += textdim.wid;
203                                 dim.des = max(dim.des - textdim.asc + dim.asc, textdim.des);
204                                 dim.asc = textdim.asc;
205                         } else {
206                                 dim.des += textdim.height() + TEXT_TO_INSET_OFFSET;
207                                 dim.wid = max(dim.wid, textdim.wid);
208                         }
209                 }
210                 break;
211         }
212
213         mi.base.font = tmpfont;
214 }
215
216
217 bool InsetCollapsable::setMouseHover(BufferView const * bv, bool mouse_hover)
218         const
219 {
220         view_[bv].mouse_hover_ = mouse_hover;
221         return true;
222 }
223
224
225 void InsetCollapsable::draw(PainterInfo & pi, int x, int y) const
226 {
227         BufferView const & bv = *pi.base.bv;
228
229         view_[&bv].auto_open_ = bv.cursor().isInside(this);
230
231         FontInfo tmpfont = pi.base.font;
232         pi.base.font = getFont();
233         pi.base.font.realize(tmpfont);
234
235         // Draw button first -- top, left or only
236         Dimension dimc = dimensionCollapsed(bv);
237
238         if (geometry(bv) == TopButton ||
239             geometry(bv) == LeftButton ||
240             geometry(bv) == ButtonOnly) {
241                 view_[&bv].button_dim_.x1 = x + 0;
242                 view_[&bv].button_dim_.x2 = x + dimc.width();
243                 view_[&bv].button_dim_.y1 = y - dimc.asc;
244                 view_[&bv].button_dim_.y2 = y + dimc.des;
245
246                 FontInfo labelfont = getLabelfont();
247                 labelfont.setColor(labelColor());
248                 pi.pain.buttonText(x, y, buttonLabel(bv), labelfont,
249                         view_[&bv].mouse_hover_);
250         } else {
251                 view_[&bv].button_dim_.x1 = 0;
252                 view_[&bv].button_dim_.y1 = 0;
253                 view_[&bv].button_dim_.x2 = 0;
254                 view_[&bv].button_dim_.y2 = 0;
255         }
256
257         Dimension const textdim = InsetText::dimension(bv);
258         int const baseline = y;
259         int textx, texty;
260         switch (geometry(bv)) {
261         case LeftButton:
262                 textx = x + dimc.width();
263                 texty = baseline;
264                 InsetText::draw(pi, textx, texty);
265                 break;
266         case TopButton:
267                 textx = x;
268                 texty = baseline + dimc.des + textdim.asc;
269                 InsetText::draw(pi, textx, texty);
270                 break;
271         case ButtonOnly:
272                 break;
273         case NoButton:
274                 textx = x;
275                 texty = baseline;
276                 InsetText::draw(pi, textx, texty);
277                 break;
278         case SubLabel:
279         case Corners:
280                 textx = x;
281                 texty = baseline;
282                 const_cast<InsetCollapsable *>(this)->setDrawFrame(false);
283                 InsetText::draw(pi, textx, texty);
284                 const_cast<InsetCollapsable *>(this)->setDrawFrame(true);
285
286                 int desc = textdim.descent();
287                 if (geometry(bv) == Corners)
288                         desc -= 3;
289
290                 const int xx1 = x + TEXT_TO_INSET_OFFSET - 1;
291                 const int xx2 = x + textdim.wid - TEXT_TO_INSET_OFFSET + 1;
292                 pi.pain.line(xx1, y + desc - 4,
293                              xx1, y + desc,
294                         Color_foreground);
295                 if (status_ == Open)
296                         pi.pain.line(xx1, y + desc,
297                                 xx2, y + desc,
298                                 Color_foreground);
299                 else {
300                         // Make status_ value visible:
301                         pi.pain.line(xx1, y + desc,
302                                 xx1 + 4, y + desc,
303                                 Color_foreground);
304                         pi.pain.line(xx2 - 4, y + desc,
305                                 xx2, y + desc,
306                                 Color_foreground);
307                 }
308                 pi.pain.line(x + textdim.wid - 3, y + desc, x + textdim.wid - 3,
309                         y + desc - 4, Color_foreground);
310
311                 // the label below the text. Can be toggled.
312                 if (geometry(bv) == SubLabel) {
313                         FontInfo font(getLabelfont());
314                         font.realize(sane_font);
315                         font.decSize();
316                         font.decSize();
317                         int w = 0;
318                         int a = 0;
319                         int d = 0;
320                         theFontMetrics(font).rectText(buttonLabel(bv), w, a, d);
321                         int const ww = max(textdim.wid, w);
322                         pi.pain.rectText(x + (ww - w) / 2, y + desc + a,
323                                 buttonLabel(bv), font, Color_none, Color_none);
324                 }
325
326                 // a visual cue when the cursor is inside the inset
327                 Cursor const & cur = bv.cursor();
328                 if (cur.isInside(this)) {
329                         y -= textdim.asc;
330                         y += 3;
331                         pi.pain.line(xx1, y + 4, xx1, y, Color_foreground);
332                         pi.pain.line(xx1 + 4, y, xx1, y, Color_foreground);
333                         pi.pain.line(xx2, y + 4, xx2, y, Color_foreground);
334                         pi.pain.line(xx2 - 4, y, xx2, y, Color_foreground);
335                 }
336                 break;
337         }
338
339         pi.base.font = tmpfont;
340 }
341
342
343 void InsetCollapsable::cursorPos(BufferView const & bv,
344                 CursorSlice const & sl, bool boundary, int & x, int & y) const
345 {
346         if (geometry(bv) == ButtonOnly)
347                 status_ = Open;
348
349         InsetText::cursorPos(bv, sl, boundary, x, y);
350         Dimension const textdim = InsetText::dimension(bv);
351
352         switch (geometry(bv)) {
353         case LeftButton:
354                 x += dimensionCollapsed(bv).wid;
355                 break;
356         case TopButton: {
357                 y += dimensionCollapsed(bv).des + textdim.asc;
358                 break;
359         }
360         case NoButton:
361         case SubLabel:
362         case Corners:
363                 // Do nothing
364                 break;
365         case ButtonOnly:
366                 // Cannot get here
367                 break;
368         }
369 }
370
371
372 bool InsetCollapsable::editable() const
373 {
374         switch (decoration()) {
375         case InsetLayout::CLASSIC:
376         case InsetLayout::MINIMALISTIC:
377                 return status_ == Open;
378         default:
379                 return true;
380         }
381 }
382
383
384 bool InsetCollapsable::descendable(BufferView const & bv) const
385 {
386         return geometry(bv) != ButtonOnly;
387 }
388
389
390 bool InsetCollapsable::clickable(BufferView const & bv, int x, int y) const
391 {
392         return view_[&bv].button_dim_.contains(x, y);
393 }
394
395
396 docstring const InsetCollapsable::getNewLabel(docstring const & l) const
397 {
398         docstring label;
399         pos_type const max_length = 15;
400         pos_type const p_siz = paragraphs().begin()->size();
401         pos_type const n = min(max_length, p_siz);
402         pos_type i = 0;
403         pos_type j = 0;
404         for (; i < n && j < p_siz; ++j) {
405                 if (paragraphs().begin()->isInset(j))
406                         continue;
407                 label += paragraphs().begin()->getChar(j);
408                 ++i;
409         }
410         if (paragraphs().size() > 1 || (i > 0 && j < p_siz)) {
411                 label += "...";
412         }
413         return label.empty() ? l : label;
414 }
415
416
417 void InsetCollapsable::edit(Cursor & cur, bool front, EntryDirection entry_from)
418 {
419         //lyxerr << "InsetCollapsable: edit left/right" << endl;
420         cur.push(*this);
421         InsetText::edit(cur, front, entry_from);
422 }
423
424
425 Inset * InsetCollapsable::editXY(Cursor & cur, int x, int y)
426 {
427         //lyxerr << "InsetCollapsable: edit xy" << endl;
428         if (geometry(cur.bv()) == ButtonOnly
429             || (view_[&cur.bv()].button_dim_.contains(x, y)
430                 && geometry(cur.bv()) != NoButton))
431                 return this;
432         cur.push(*this);
433         return InsetText::editXY(cur, x, y);
434 }
435
436
437 void InsetCollapsable::doDispatch(Cursor & cur, FuncRequest & cmd)
438 {
439         //lyxerr << "InsetCollapsable::doDispatch (begin): cmd: " << cmd
440         //      << " cur: " << cur << " bvcur: " << cur.bv().cursor() << endl;
441
442         bool const hitButton = clickable(cur.bv(), cmd.x(), cmd.y());
443
444         switch (cmd.action()) {
445         case LFUN_MOUSE_PRESS:
446                 if (hitButton) {
447                         switch (cmd.button()) {
448                         case mouse_button::button1:
449                         case mouse_button::button3:
450                                 // Pass the command to the enclosing InsetText,
451                                 // so that the cursor gets set.
452                                 cur.undispatched();
453                                 break;
454                         case mouse_button::none:
455                         case mouse_button::button2:
456                         case mouse_button::button4:
457                         case mouse_button::button5:
458                                 // Nothing to do.
459                                 cur.noScreenUpdate();
460                                 break;
461                         }
462                 } else if (geometry(cur.bv()) != ButtonOnly)
463                         InsetText::doDispatch(cur, cmd);
464                 else
465                         cur.undispatched();
466                 break;
467
468         case LFUN_MOUSE_MOTION:
469         case LFUN_MOUSE_DOUBLE:
470         case LFUN_MOUSE_TRIPLE:
471                 if (hitButton)
472                         cur.noScreenUpdate();
473                 else if (geometry(cur.bv()) != ButtonOnly)
474                         InsetText::doDispatch(cur, cmd);
475                 else
476                         cur.undispatched();
477                 break;
478
479         case LFUN_MOUSE_RELEASE:
480                 if (!hitButton) {
481                         // The mouse click has to be within the inset!
482                         if (geometry(cur.bv()) != ButtonOnly)
483                                 InsetText::doDispatch(cur, cmd);
484                         else
485                                 cur.undispatched();
486                         break;
487                 }
488                 if (cmd.button() != mouse_button::button1) {
489                         // Nothing to do.
490                         cur.noScreenUpdate();
491                         break;
492                 }
493                 // if we are selecting, we do not want to
494                 // toggle the inset.
495                 if (cur.selection())
496                         break;
497                 // Left button is clicked, the user asks to
498                 // toggle the inset visual state.
499                 cur.dispatched();
500                 cur.screenUpdateFlags(Update::Force | Update::FitCursor);
501                 if (geometry(cur.bv()) == ButtonOnly) {
502                         setStatus(cur, Open);
503                         edit(cur, true);
504                 }
505                 else
506                         setStatus(cur, Collapsed);
507                 cur.bv().cursor() = cur;
508                 break;
509
510         case LFUN_INSET_TOGGLE:
511                 if (cmd.argument() == "open")
512                         setStatus(cur, Open);
513                 else if (cmd.argument() == "close")
514                         setStatus(cur, Collapsed);
515                 else if (cmd.argument() == "toggle" || cmd.argument().empty())
516                         if (status_ == Open)
517                                 setStatus(cur, Collapsed);
518                         else
519                                 setStatus(cur, Open);
520                 else // if assign or anything else
521                         cur.undispatched();
522                 cur.dispatched();
523                 break;
524
525         default:
526                 InsetText::doDispatch(cur, cmd);
527                 break;
528         }
529 }
530
531
532 bool InsetCollapsable::getStatus(Cursor & cur, FuncRequest const & cmd,
533                 FuncStatus & flag) const
534 {
535         switch (cmd.action()) {
536         case LFUN_INSET_TOGGLE:
537                 if (cmd.argument() == "open")
538                         flag.setEnabled(status_ != Open);
539                 else if (cmd.argument() == "close")
540                         flag.setEnabled(status_ == Open);
541                 else if (cmd.argument() == "toggle" || cmd.argument().empty()) {
542                         flag.setEnabled(true);
543                         flag.setOnOff(status_ == Open);
544                 } else
545                         flag.setEnabled(false);
546                 return true;
547
548         default:
549                 return InsetText::getStatus(cur, cmd, flag);
550         }
551 }
552
553
554 void InsetCollapsable::setLabel(docstring const & l)
555 {
556         labelstring_ = l;
557 }
558
559
560 docstring const InsetCollapsable::buttonLabel(BufferView const & bv) const
561 {
562         InsetLayout const & il = getLayout();
563         docstring const label = labelstring_.empty() ?
564                 translateIfPossible(il.labelstring()) : labelstring_;
565         if (!il.contentaslabel() || geometry(bv) != ButtonOnly)
566                 return label;
567         return getNewLabel(label);
568 }
569
570
571 void InsetCollapsable::setStatus(Cursor & cur, CollapseStatus status)
572 {
573         status_ = status;
574         setButtonLabel();
575         if (status_ == Collapsed)
576                 cur.leaveInset(*this);
577 }
578
579
580 InsetLayout::InsetDecoration InsetCollapsable::decoration() const
581 {
582         InsetLayout::InsetDecoration const dec = getLayout().decoration();
583         return dec == InsetLayout::DEFAULT ? InsetLayout::CLASSIC : dec;
584 }
585
586
587 string InsetCollapsable::contextMenu(BufferView const & bv, int x,
588         int y) const
589 {
590         string context_menu = contextMenuName();
591         string const it_context_menu = InsetText::contextMenuName();
592         if (decoration() == InsetLayout::CONGLOMERATE)
593                 return context_menu + ";" + it_context_menu;
594
595         string const ic_context_menu = InsetCollapsable::contextMenuName();
596         if (ic_context_menu != context_menu)
597                 context_menu += ";" + ic_context_menu;
598
599         if (geometry(bv) == NoButton)
600                 return context_menu + ";" + it_context_menu;
601
602         Dimension dim = dimensionCollapsed(bv);
603         if (x < xo(bv) + dim.wid && y < yo(bv) + dim.des)
604                 return context_menu;
605
606         return it_context_menu;
607 }
608
609
610 string InsetCollapsable::contextMenuName() const
611 {
612         if (decoration() == InsetLayout::CONGLOMERATE)
613                 return "context-conglomerate";
614         else
615                 return "context-collapsable";
616 }
617
618
619 bool InsetCollapsable::canPaintChange(BufferView const & bv) const
620 {
621         switch (geometry(bv)) {
622         case Corners:
623         case SubLabel:
624         case ButtonOnly:
625                 // these cases are handled by RowPainter since the inset is inline.
626                 return false;
627         default:
628                 break;
629         }
630         // TODO: implement the drawing in the remaining cases
631         return true;
632 }
633
634
635 } // namespace lyx