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