]> git.lyx.org Git - lyx.git/blob - src/insets/InsetCollapsable.cpp
header cleanup.
[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 "BufferParams.h"
19 #include "BufferView.h"
20 #include "Cursor.h"
21 #include "debug.h"
22 #include "DispatchResult.h"
23 #include "FloatList.h"
24 #include "FuncStatus.h"
25 #include "gettext.h"
26 #include "Color.h"
27 #include "LaTeXFeatures.h"
28 #include "Lexer.h"
29 #include "FuncRequest.h"
30 #include "MetricsInfo.h"
31
32 #include "frontends/FontMetrics.h"
33 #include "frontends/Painter.h"
34
35
36 namespace lyx {
37
38 using graphics::PreviewLoader;
39
40 using std::endl;
41 using std::string;
42 using std::max;
43 using std::min;
44 using std::ostream;
45
46
47 InsetCollapsable::CollapseStatus InsetCollapsable::status() const
48 {
49         return autoOpen_ ? Open : status_;
50 }
51
52
53 InsetCollapsable::Geometry InsetCollapsable::geometry() const
54 {
55         switch (decoration()) {
56         case Classic:
57                 if (status_ == Open || autoOpen_) {
58                         if (openinlined_)
59                                 return LeftButton;
60                         else
61                                 return TopButton;
62                 } else
63                         return ButtonOnly;
64
65         case Minimalistic:
66                 return NoButton;
67
68         case Conglomerate:
69                 return status_ == Open ? SubLabel : Corners;
70         }
71
72         // dummy return value to shut down a warning,
73         // this is dead code.
74         return NoButton;
75 }
76
77
78 InsetCollapsable::InsetCollapsable
79                 (BufferParams const & bp, CollapseStatus status)
80         : InsetText(bp), status_(status),
81           openinlined_(false), autoOpen_(false), mouse_hover_(false)
82 {
83         setAutoBreakRows(true);
84         setDrawFrame(true);
85         setFrameColor(Color::collapsableframe);
86         setButtonLabel();
87         // Fallback for lacking inset layout item
88         layout_.bgcolor = Color::background;
89 }
90
91
92 InsetCollapsable::InsetCollapsable(InsetCollapsable const & rhs)
93         : InsetText(rhs),
94                 button_dim(rhs.button_dim),
95                 topx(rhs.topx),
96                 topbaseline(rhs.topbaseline),
97                 layout_(rhs.layout_),
98                 status_(rhs.status_),
99                 openinlined_(rhs.openinlined_),
100                 autoOpen_(rhs.autoOpen_),
101                 textdim_(rhs.textdim_),
102                 // the sole purpose of this copy constructor
103                 mouse_hover_(false)
104 {
105 }
106
107
108 void  InsetCollapsable::setLayout(BufferParams const & bp)
109 {
110         layout_ = getLayout(bp);
111 }
112
113
114 void InsetCollapsable::write(Buffer const & buf, ostream & os) const
115 {
116         os << "status ";
117         switch (status_) {
118         case Open:
119                 os << "open";
120                 break;
121         case Collapsed:
122                 os << "collapsed";
123                 break;
124         }
125         os << "\n";
126         text_.write(buf, os);
127 }
128
129
130 void InsetCollapsable::read(Buffer const & buf, Lexer & lex)
131 {
132         bool token_found = false;
133         if (lex.isOK()) {
134                 lex.next();
135                 string const token = lex.getString();
136                 if (token == "status") {
137                         lex.next();
138                         string const tmp_token = lex.getString();
139
140                         if (tmp_token == "collapsed") {
141                                 status_ = Collapsed;
142                                 token_found = true;
143                         } else if (tmp_token == "open") {
144                                 status_ = Open;
145                                 token_found = true;
146                         } else {
147                                 lyxerr << "InsetCollapsable::read: Missing status!"
148                                        << endl;
149                                 // Take countermeasures
150                                 lex.pushToken(token);
151                         }
152                 } else {
153                         lyxerr << "InsetCollapsable::read: Missing 'status'-tag!"
154                                    << endl;
155                         // take countermeasures
156                         lex.pushToken(token);
157                 }
158         }
159         InsetText::read(buf, lex);
160
161         if (!token_found)
162                 status_ = isOpen() ? Open : Collapsed;
163
164         setButtonLabel();
165 }
166
167
168 Dimension InsetCollapsable::dimensionCollapsed() const
169 {
170         Dimension dim;
171         theFontMetrics(layout_.labelfont).buttonText(
172                 layout_.labelstring, dim.wid, dim.asc, dim.des);
173         return dim;
174 }
175
176
177 bool InsetCollapsable::metrics(MetricsInfo & mi, Dimension & dim) const
178 {
179         autoOpen_ = mi.base.bv->cursor().isInside(this);
180         mi.base.textwidth -= (int) (1.5 * TEXT_TO_INSET_OFFSET);
181
182         switch (decoration()) {
183         case Minimalistic:
184                 InsetText::metrics(mi, dim);
185                 break;
186         case Conglomerate:
187                 InsetText::metrics(mi, dim);
188                 if (status() == Open) {
189                         // consider width of the inset label
190                         Font font(layout_.labelfont);
191                         font.realize(Font(Font::ALL_SANE));
192                         font.decSize();
193                         font.decSize();
194                         int w = 0;
195                         int a = 0;
196                         int d = 0;
197                         docstring s = layout_.labelstring;
198                         theFontMetrics(font).rectText(s, w, a, d);
199                         dim.wid = max(dim.wid, w);
200                 }
201                 if (status() == Open)
202                         dim.des += ascent();
203                 else {
204                         dim.des -= 3;
205                         dim.asc -= 3;
206                 }
207                 break;
208         case Classic:
209                 dim = dimensionCollapsed();
210                 if (geometry() == TopButton
211                  || geometry() == LeftButton) {
212                         InsetText::metrics(mi, textdim_);
213                         // This expression should not contain mi.base.texwidth
214                         openinlined_ = !hasFixedWidth()
215                                 && textdim_.wid < 0.5 * mi.base.bv->workWidth();
216                         if (openinlined_) {
217                                 // Correct for button width, and re-fit
218                                 mi.base.textwidth -= dim.wid;
219                                 InsetText::metrics(mi, textdim_);
220                                 dim.wid += textdim_.wid;
221                                 dim.des = max(dim.des - textdim_.asc + dim.asc, textdim_.des);
222                                 dim.asc = textdim_.asc;
223                         } else {
224                                 dim.des += textdim_.height() + TEXT_TO_BOTTOM_OFFSET;
225                                 dim.wid = max(dim.wid, textdim_.wid);
226                                 if (hasFixedWidth())
227                                         dim.wid = max(dim.wid, mi.base.textwidth);
228                         }
229                 }
230                 break;
231         }
232         dim.asc += TEXT_TO_INSET_OFFSET;
233         dim.des += TEXT_TO_INSET_OFFSET;
234         dim.wid += (int) (1.5 * TEXT_TO_INSET_OFFSET);
235         mi.base.textwidth += (int) (1.5 * TEXT_TO_INSET_OFFSET);
236         bool const changed = dim_ != dim;
237         dim_ = dim;
238         return changed;
239 }
240
241
242 bool InsetCollapsable::setMouseHover(bool mouse_hover)
243 {
244         mouse_hover_ = mouse_hover;
245         return true;
246 }
247
248
249 void InsetCollapsable::draw(PainterInfo & pi, int x, int y) const
250 {
251         autoOpen_ = pi.base.bv->cursor().isInside(this);
252         text_.background_color_ = backgroundColor();
253         const int xx = x + TEXT_TO_INSET_OFFSET;
254
255         // Draw button first -- top, left or only
256         Dimension dimc = dimensionCollapsed();
257         int const top  = y - ascent() + TEXT_TO_INSET_OFFSET;
258         if (decoration() == Classic) {
259                 button_dim.x1 = xx + 0;
260                 button_dim.x2 = xx + dimc.width();
261                 button_dim.y1 = top;
262                 button_dim.y2 = top + dimc.height();
263
264                 pi.pain.buttonText(xx, top + dimc.asc, layout_.labelstring, layout_.labelfont, mouse_hover_);
265         }
266
267         int textx, texty;
268         switch (geometry()) {
269         case LeftButton:
270                 textx = xx + dimc.width();
271                 texty = top + textdim_.asc;
272                 InsetText::draw(pi, textx, texty);
273                 break;
274         case TopButton:
275                 textx = xx;
276                 texty = top + dimc.height() + textdim_.asc;
277                 InsetText::draw(pi, textx, texty);
278                 break;
279         case ButtonOnly:
280                 break;
281         case NoButton:
282                 textx = xx;
283                 texty = y + textdim_.asc;
284                 InsetText::draw(pi, textx, texty);
285                 break;
286         case SubLabel:
287         case Corners:
288                 textx = xx;
289                 texty = y + textdim_.asc;
290                 const_cast<InsetCollapsable *>(this)->setDrawFrame(false);
291                 InsetText::draw(pi, textx, texty);
292                 const_cast<InsetCollapsable *>(this)->setDrawFrame(true);
293
294                 int desc = InsetText::descent();
295                 if (status() == Open)
296                         desc -= ascent();
297                 else
298                         desc -= 3;
299
300                 pi.pain.line(x, y + desc - 4, x, y + desc, 
301                         layout_.labelfont.color());
302                 if (internalStatus() == Open)
303                         pi.pain.line(x, y + desc, 
304                                 x + dim_.wid - 3, y + desc,
305                                 layout_.labelfont.color());
306                 else {
307                         // Make status_ value visible:
308                         pi.pain.line(x, y + desc,
309                                 x + 4, y + desc,
310                                 layout_.labelfont.color());
311                         pi.pain.line(x + dim_.wid - 7, y + desc,
312                                 x + dim_.wid -3, y + desc,
313                                 layout_.labelfont.color());
314                 }
315                 pi.pain.line(x + dim_.wid - 3, y + desc, x + dim_.wid - 3, y + desc - 4,
316                         layout_.labelfont.color());
317
318                 // the label of the charstyle. Can be toggled.
319                 if (status() == Open) {
320                         Font font(layout_.labelfont);
321                         font.realize(Font(Font::ALL_SANE));
322                         font.decSize();
323                         font.decSize();
324                         int w = 0;
325                         int a = 0;
326                         int d = 0;
327                         // FIXME UNICODE
328                         docstring s = layout_.labelstring;
329                         theFontMetrics(font).rectText(s, w, a, d);
330                         pi.pain.rectText(x + (dim_.wid - w) / 2, y + desc + a,
331                                 s, font, Color::none, Color::none);
332                 }
333
334                 // a visual cue when the cursor is inside the inset
335                 Cursor & cur = pi.base.bv->cursor();
336                 if (cur.isInside(this)) {
337                         y -= ascent();
338                         y += 3;
339                         pi.pain.line(x, y + 4, x, y, layout_.labelfont.color());
340                         pi.pain.line(x + 4, y, x, y, layout_.labelfont.color());
341                         pi.pain.line(x + dim_.wid - 3, y + 4, x + dim_.wid - 3, y,
342                                 layout_.labelfont.color());
343                         pi.pain.line(x + dim_.wid - 7, y, x + dim_.wid - 3, y,
344                                 layout_.labelfont.color());
345                 }
346                 break;
347         }
348         setPosCache(pi, x, y);
349 }
350
351
352 void InsetCollapsable::drawSelection(PainterInfo & pi, int x, int y) const
353 {
354         x += TEXT_TO_INSET_OFFSET;
355         switch (geometry()) {
356         case LeftButton:
357                 x += dimensionCollapsed().wid;
358                 InsetText::drawSelection(pi, x, y);
359                 break;
360         case TopButton:
361                 y += dimensionCollapsed().des + textdim_.asc;
362                 InsetText::drawSelection(pi, x, y);
363                 break;
364         case ButtonOnly:
365                 break;
366         case NoButton:
367         case SubLabel:
368         case Corners:
369                 InsetText::drawSelection(pi, x, y);
370                 break;
371         }
372 }
373
374
375 void InsetCollapsable::cursorPos(BufferView const & bv,
376                 CursorSlice const & sl, bool boundary, int & x, int & y) const
377 {
378         BOOST_ASSERT(geometry() != ButtonOnly);
379
380         InsetText::cursorPos(bv, sl, boundary, x, y);
381
382         switch (geometry()) {
383         case LeftButton:
384                 x += dimensionCollapsed().wid;
385                 break;
386         case TopButton:
387                 y += dimensionCollapsed().height() - ascent()
388                         + TEXT_TO_INSET_OFFSET + textdim_.asc;
389                 break;
390         case NoButton:
391         case SubLabel:
392         case Corners:
393                 // Do nothing
394                 break;
395         case ButtonOnly:
396                 // Cannot get here
397                 break;
398         }
399         x += TEXT_TO_INSET_OFFSET;
400 }
401
402
403 Inset::EDITABLE InsetCollapsable::editable() const
404 {
405         return geometry() != ButtonOnly? HIGHLY_EDITABLE : IS_EDITABLE;
406 }
407
408
409 bool InsetCollapsable::descendable() const
410 {
411         return geometry() != ButtonOnly;
412 }
413
414
415 bool InsetCollapsable::hitButton(FuncRequest const & cmd) const
416 {
417         return button_dim.contains(cmd.x, cmd.y);
418 }
419
420
421 docstring const InsetCollapsable::getNewLabel(docstring const & l) const
422 {
423         docstring label;
424         pos_type const max_length = 15;
425         pos_type const p_siz = paragraphs().begin()->size();
426         pos_type const n = min(max_length, p_siz);
427         pos_type i = 0;
428         pos_type j = 0;
429         for (; i < n && j < p_siz; ++j) {
430                 if (paragraphs().begin()->isInset(j))
431                         continue;
432                 label += paragraphs().begin()->getChar(j);
433                 ++i;
434         }
435         if (paragraphs().size() > 1 || (i > 0 && j < p_siz)) {
436                 label += "...";
437         }
438         return label.empty() ? l : label;
439 }
440
441
442 void InsetCollapsable::edit(Cursor & cur, bool left)
443 {
444         //lyxerr << "InsetCollapsable: edit left/right" << endl;
445         cur.push(*this);
446         InsetText::edit(cur, left);
447 }
448
449
450 Inset * InsetCollapsable::editXY(Cursor & cur, int x, int y)
451 {
452         //lyxerr << "InsetCollapsable: edit xy" << endl;
453         if (geometry() == ButtonOnly
454          || (button_dim.contains(x, y) 
455           && decoration() != Minimalistic))
456                 return this;
457         cur.push(*this);
458         return InsetText::editXY(cur, x, y);
459 }
460
461
462 void InsetCollapsable::doDispatch(Cursor & cur, FuncRequest & cmd)
463 {
464         //lyxerr << "InsetCollapsable::doDispatch (begin): cmd: " << cmd
465         //      << " cur: " << cur << " bvcur: " << cur.bv().cursor() << endl;
466
467         switch (cmd.action) {
468         case LFUN_MOUSE_PRESS:
469                 if (cmd.button() == mouse_button::button1 
470                  && hitButton(cmd) 
471                  && decoration() != Minimalistic) {
472                         // reset selection if necessary (see bug 3060)
473                         if (cur.selection())
474                                 cur.bv().cursor().clearSelection();
475                         else
476                                 cur.noUpdate();
477                         cur.dispatched();
478                         break;
479                 }
480                 if (decoration() == Minimalistic)
481                         InsetText::doDispatch(cur, cmd);
482                 else if (geometry() != ButtonOnly 
483                      && !hitButton(cmd))
484                         InsetText::doDispatch(cur, cmd);
485                 else
486                         cur.undispatched();
487                 break;
488
489         case LFUN_MOUSE_MOTION:
490         case LFUN_MOUSE_DOUBLE:
491         case LFUN_MOUSE_TRIPLE:
492                 if (decoration() == Minimalistic)
493                         InsetText::doDispatch(cur, cmd);
494                 else if (geometry() != ButtonOnly
495                      && !hitButton(cmd))
496                         InsetText::doDispatch(cur, cmd);
497                 else
498                         cur.undispatched();
499                 break;
500
501         case LFUN_MOUSE_RELEASE:
502                 if (cmd.button() == mouse_button::button3) {
503                         if (decoration() == Conglomerate) {
504                                 if (internalStatus() == Open)
505                                         setStatus(cur, Collapsed);
506                                 else
507                                         setStatus(cur, Open);
508                                 break;
509                         } else {
510                                 // Open the Inset 
511                                 // configuration dialog
512                                 showInsetDialog(&cur.bv());
513                                 break;
514                         }
515                 }
516
517                 if (decoration() == Minimalistic) {
518                         // The mouse click has to be within the inset!
519                         InsetText::doDispatch(cur, cmd);
520                         break;
521                 }
522
523                 if (cmd.button() == mouse_button::button1 && hitButton(cmd)) {
524                         // if we are selecting, we do not want to
525                         // toggle the inset.
526                         if (cur.selection())
527                                 break;
528                         // Left button is clicked, the user asks to
529                         // toggle the inset visual state.
530                         cur.dispatched();
531                         cur.updateFlags(Update::Force | Update::FitCursor);
532                         if (geometry() == ButtonOnly) {
533                                 setStatus(cur, Open);
534                                 edit(cur, true);
535                         }
536                         else {
537                                 setStatus(cur, Collapsed);
538                         }
539                         cur.bv().cursor() = cur;
540                         break;
541                 }
542
543                 // The mouse click is within the opened inset.
544                 if (geometry() == TopButton
545                  || geometry() == LeftButton)
546                         InsetText::doDispatch(cur, cmd);
547                 break;
548
549         case LFUN_INSET_TOGGLE:
550                 if (cmd.argument() == "open")
551                         setStatus(cur, Open);
552                 else if (cmd.argument() == "close")
553                         setStatus(cur, Collapsed);
554                 else if (cmd.argument() == "toggle" || cmd.argument().empty())
555                         if (internalStatus() == Open) {
556                                 setStatus(cur, Collapsed);
557                                 if (geometry() == ButtonOnly)
558                                         cur.top().forwardPos();
559                         } else
560                                 setStatus(cur, Open);
561                 else // if assign or anything else
562                         cur.undispatched();
563                 cur.dispatched();
564                 break;
565
566         default:
567                 InsetText::doDispatch(cur, cmd);
568                 break;
569         }
570 }
571
572
573 bool InsetCollapsable::getStatus(Cursor & cur, FuncRequest const & cmd,
574                 FuncStatus & flag) const
575 {
576         switch (cmd.action) {
577
578         case LFUN_INSET_TOGGLE:
579                 if (cmd.argument() == "open" || cmd.argument() == "close" ||
580                     cmd.argument() == "toggle")
581                         flag.enabled(true);
582                 else
583                         flag.enabled(false);
584                 return true;
585
586         default:
587                 return InsetText::getStatus(cur, cmd, flag);
588         }
589 }
590
591
592 void InsetCollapsable::setLabel(docstring const & l)
593 {
594         layout_.labelstring = l;
595 }
596
597
598 void InsetCollapsable::setStatus(Cursor & cur, CollapseStatus status)
599 {
600         status_ = status;
601         setButtonLabel();
602         if (status_ == Collapsed)
603                 cur.leaveInset(*this);
604         // Because the collapse status is part of the inset and thus an
605         // integral part of the Buffer contents a changed status must be
606         // signaled to all views of current buffer.
607         cur.bv().buffer().changed();
608 }
609
610
611 void InsetCollapsable::setLabelFont(Font const & font)
612 {
613         layout_.labelfont = font;
614 }
615
616 docstring InsetCollapsable::floatName(string const & type, BufferParams const & bp) const
617 {
618         FloatList const & floats = bp.getTextClass().floats();
619         FloatList::const_iterator it = floats[type];
620         // FIXME UNICODE
621         return (it == floats.end()) ? from_ascii(type) : bp.B_(it->second.name());
622 }
623
624
625 InsetCollapsable::Decoration InsetCollapsable::decoration() const
626 {
627         if (layout_.decoration == "classic")
628                 return Classic;
629         if (layout_.decoration == "minimalistic")
630                 return Minimalistic;
631         if (layout_.decoration == "conglomerate")
632                 return Conglomerate;
633         if (name() == from_ascii("CharStyle"))
634                 return Conglomerate;
635         return Classic;
636 }
637
638
639 int InsetCollapsable::latex(Buffer const & buf, odocstream & os,
640                           OutputParams const & runparams) const
641 {
642         // This implements the standard way of handling the LaTeX output of
643         // a collapsable inset, either a command or an environment. Standard 
644         // collapsable insets should not redefine this, non-standard ones may
645         // call this.
646         if (!layout_.latexname.empty()) {
647                 if (layout_.latextype == "command") {
648                         // FIXME UNICODE
649                         os << '\\' << from_utf8(layout_.latexname);
650                         if (!layout_.latexparam.empty())
651                                 os << from_utf8(layout_.latexparam);
652                         os << '{';
653                 } else if (layout_.latextype == "environment") {
654                         os << "%\n\\begin{" << from_utf8(layout_.latexname) << "}\n";
655                         if (!layout_.latexparam.empty())
656                                 os << from_utf8(layout_.latexparam);
657                 }
658         }
659         int i = InsetText::latex(buf, os, runparams);
660         if (!layout_.latexname.empty())
661                 if (layout_.latextype == "command") {
662                         os << "}";
663                 } else if (layout_.latextype == "environment") {
664                         os << "\n\\end{" << from_utf8(layout_.latexname) << "}\n";
665                         i += 4;
666                 }
667         return i;
668 }
669
670
671 void InsetCollapsable::validate(LaTeXFeatures & features) const
672 {
673         // Force inclusion of preamble snippet in layout file
674         features.addPreambleSnippet(layout_.preamble);
675         InsetText::validate(features);
676 }
677
678
679 } // namespace lyx