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