]> git.lyx.org Git - lyx.git/blob - src/CutAndPaste.C
- Link against qt-mt333.lib which is what the current qt3 cvs produces
[lyx.git] / src / CutAndPaste.C
1 /*
2  * \file CutAndPaste.C
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Jürgen Vigna
7  * \author Lars Gullik Bjønnes
8  * \author Alfredo Braunstein
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "CutAndPaste.h"
16
17 #include "buffer.h"
18 #include "buffer_funcs.h"
19 #include "bufferparams.h"
20 #include "BufferView.h"
21 #include "cursor.h"
22 #include "debug.h"
23 #include "errorlist.h"
24 #include "funcrequest.h"
25 #include "gettext.h"
26 #include "lfuns.h"
27 #include "lyxrc.h"
28 #include "lyxtext.h"
29 #include "lyxtextclasslist.h"
30 #include "paragraph.h"
31 #include "paragraph_funcs.h"
32 #include "ParagraphParameters.h"
33 #include "ParagraphList_fwd.h"
34 #include "pariterator.h"
35 #include "undo.h"
36
37 #include "insets/insettabular.h"
38
39 #include "mathed/math_data.h"
40 #include "mathed/math_inset.h"
41 #include "mathed/math_support.h"
42
43 #include "support/lstrings.h"
44
45 #include <boost/tuple/tuple.hpp>
46
47 using lyx::pos_type;
48 using lyx::pit_type;
49 using lyx::textclass_type;
50
51 using lyx::support::bformat;
52
53 using std::endl;
54 using std::for_each;
55 using std::make_pair;
56 using std::pair;
57 using std::vector;
58 using std::string;
59
60
61 namespace {
62
63 typedef std::pair<lyx::pit_type, int> PitPosPair;
64
65 typedef limited_stack<pair<ParagraphList, textclass_type> > CutStack;
66
67 CutStack theCuts(10);
68
69 class resetOwnerAndChanges : public std::unary_function<Paragraph, void> {
70 public:
71         void operator()(Paragraph & p) const {
72                 p.cleanChanges();
73                 p.setInsetOwner(0);
74         }
75 };
76
77
78 void region(CursorSlice const & i1, CursorSlice const & i2,
79         InsetBase::row_type & r1, InsetBase::row_type & r2,
80         InsetBase::col_type & c1, InsetBase::col_type & c2)
81 {
82         InsetBase & p = i1.inset();
83         c1 = p.col(i1.idx());
84         c2 = p.col(i2.idx());
85         if (c1 > c2)
86                 std::swap(c1, c2);
87         r1 = p.row(i1.idx());
88         r2 = p.row(i2.idx());
89         if (r1 > r2)
90                 std::swap(r1, r2);
91 }
92
93
94 bool checkPastePossible(int index)
95 {
96         return size_t(index) < theCuts.size() && !theCuts[index].first.empty();
97 }
98
99
100 pair<PitPosPair, pit_type>
101 pasteSelectionHelper(Buffer const & buffer, ParagraphList & pars,
102         pit_type pit, int pos,
103         textclass_type tc, size_t cut_index, ErrorList & errorlist)
104 {
105         if (!checkPastePossible(cut_index))
106                 return make_pair(PitPosPair(pit, pos), pit);
107
108         BOOST_ASSERT (pos <= pars[pit].size());
109
110         // Make a copy of the CaP paragraphs.
111         ParagraphList insertion = theCuts[cut_index].first;
112         textclass_type const textclass = theCuts[cut_index].second;
113
114         // Now remove all out of the pars which is NOT allowed in the
115         // new environment and set also another font if that is required.
116
117         // Make sure there is no class difference.
118         lyx::cap::SwitchLayoutsBetweenClasses(textclass, tc, insertion,
119                                     errorlist);
120
121         ParagraphList::iterator tmpbuf = insertion.begin();
122         int depth_delta = pars[pit].params().depth() - tmpbuf->params().depth();
123
124         Paragraph::depth_type max_depth = pars[pit].getMaxDepthAfter();
125
126         for (; tmpbuf != insertion.end(); ++tmpbuf) {
127                 // If we have a negative jump so that the depth would
128                 // go below 0 depth then we have to redo the delta to
129                 // this new max depth level so that subsequent
130                 // paragraphs are aligned correctly to this paragraph
131                 // at level 0.
132                 if (int(tmpbuf->params().depth()) + depth_delta < 0)
133                         depth_delta = 0;
134
135                 // Set the right depth so that we are not too deep or shallow.
136                 tmpbuf->params().depth(tmpbuf->params().depth() + depth_delta);
137                 if (tmpbuf->params().depth() > max_depth)
138                         tmpbuf->params().depth(max_depth);
139
140                 // Only set this from the 2nd on as the 2nd depends
141                 // for maxDepth still on pit.
142                 if (tmpbuf != insertion.begin())
143                         max_depth = tmpbuf->getMaxDepthAfter();
144
145                 // Set the inset owner of this paragraph.
146                 tmpbuf->setInsetOwner(pars[pit].inInset());
147                 for (pos_type i = 0; i < tmpbuf->size(); ++i) {
148                         if (tmpbuf->getChar(i) == Paragraph::META_INSET) {
149                                 if (!pars[pit].insetAllowed(tmpbuf->getInset(i)->lyxCode()))
150                                         tmpbuf->erase(i--);
151                         }
152                 }
153         }
154
155         bool const empty = pars[pit].empty();
156         if (!empty) {
157                 // Make the buf exactly the same layout as the cursor
158                 // paragraph.
159                 insertion.begin()->makeSameLayout(pars[pit]);
160         }
161
162         // Prepare the paragraphs and insets for insertion.
163         // A couple of insets store buffer references so need updating.
164         InsetText in;
165         std::swap(in.paragraphs(), insertion);
166
167         ParIterator fpit = par_iterator_begin(in);
168         ParIterator fend = par_iterator_end(in);
169
170         for (; fpit != fend; ++fpit) {
171                 InsetList::iterator lit = fpit->insetlist.begin();
172                 InsetList::iterator eit = fpit->insetlist.end();
173
174                 for (; lit != eit; ++lit) {
175                         switch (lit->inset->lyxCode()) {
176                         case InsetBase::TABULAR_CODE: {
177                                 InsetTabular * it = static_cast<InsetTabular*>(lit->inset);
178                                 it->buffer(&buffer);
179                                 break;
180                         }
181
182                         default:
183                                 break; // nothing
184                         }
185                 }
186         }
187         std::swap(in.paragraphs(), insertion);
188
189         // Split the paragraph for inserting the buf if necessary.
190         if (!empty)
191                 breakParagraphConservative(buffer.params(), pars, pit, pos);
192
193         // Paste it!
194         if (empty) {
195                 pars.insert(pars.begin() + pit, insertion.begin(),
196                             insertion.end());
197
198                 // merge the empty par with the last par of the insertion
199                 mergeParagraph(buffer.params(), pars,
200                                pit + insertion.size() - 1);
201         } else {
202                 pars.insert(pars.begin() + pit + 1, insertion.begin(),
203                             insertion.end());
204
205                 // merge the first par of the insertion with the current par
206                 mergeParagraph(buffer.params(), pars, pit);
207         }
208
209         pit_type last_paste = pit + insertion.size() - 1;
210
211         // Store the new cursor position.
212         pit = last_paste;
213         pos = pars[last_paste].size();
214
215         // Maybe some pasting.
216         if (!empty && last_paste + 1 != pit_type(pars.size())) {
217                 if (pars[last_paste + 1].hasSameLayout(pars[last_paste])) {
218                         mergeParagraph(buffer.params(), pars, last_paste);
219                 } else if (pars[last_paste + 1].empty()) {
220                         pars[last_paste + 1].makeSameLayout(pars[last_paste]);
221                         mergeParagraph(buffer.params(), pars, last_paste);
222                 } else if (pars[last_paste].empty()) {
223                         pars[last_paste].makeSameLayout(pars[last_paste + 1]);
224                         mergeParagraph(buffer.params(), pars, last_paste);
225                 } else {
226                         pars[last_paste + 1].stripLeadingSpaces();
227                         ++last_paste;
228                 }
229         }
230
231         return make_pair(PitPosPair(pit, pos), last_paste + 1);
232 }
233
234
235 PitPosPair eraseSelectionHelper(BufferParams const & params,
236         ParagraphList & pars,
237         pit_type startpit, pit_type endpit,
238         int startpos, int endpos, bool doclear)
239 {
240         if (startpit == pit_type(pars.size()) ||
241             (startpos > pars[startpit].size()))
242                 return PitPosPair(endpit, endpos);
243
244         if (endpit == pit_type(pars.size()) ||
245             startpit == endpit) {
246                 endpos -= pars[startpit].erase(startpos, endpos);
247                 return PitPosPair(endpit, endpos);
248         }
249
250         // clear end/begin fragments of the first/last par in selection
251         bool all_erased = true;
252
253         pars[startpit].erase(startpos, pars[startpit].size());
254         if (pars[startpit].size() != startpos)
255                 all_erased = false;
256
257         endpos -= pars[endpit].erase(0, endpos);
258         if (endpos != 0)
259                 all_erased = false;
260
261         // Loop through the deleted pars if any, erasing as needed
262         for (pit_type pit = startpit + 1; pit != endpit;) {
263                 // "erase" the contents of the par
264                 pars[pit].erase(0, pars[pit].size());
265                 if (!pars[pit].size()) {
266                         // remove the par if it's now empty
267                         pars.erase(pars.begin() + pit);
268                         --endpit;
269                 } else {
270                         ++pit;
271                         all_erased = false;
272                 }
273         }
274
275 #if 0 // FIXME: why for cut but not copy ?
276         // the cut selection should begin with standard layout
277         if (realcut) {
278                 buf->params().clear();
279                 buf->bibkey = 0;
280                 buf->layout(textclasslist[buffer->params.textclass].defaultLayoutName());
281         }
282 #endif
283
284         if (startpit + 1 == pit_type(pars.size()))
285                 return PitPosPair(endpit, endpos);
286
287         if (doclear) {
288                 pars[startpit + 1].stripLeadingSpaces();
289         }
290
291         // paste the paragraphs again, if possible
292         if (all_erased &&
293             (pars[startpit].hasSameLayout(pars[startpit + 1]) ||
294              pars[startpit + 1].empty())) {
295                 mergeParagraph(params, pars, startpit);
296                 // this because endpar gets deleted here!
297                 endpit = startpit;
298                 endpos = startpos;
299         }
300
301         return PitPosPair(endpit, endpos);
302
303 }
304
305
306 void copySelectionHelper(ParagraphList & pars,
307         pit_type startpit, pit_type endpit,
308         int start, int end, textclass_type tc)
309 {
310         BOOST_ASSERT(0 <= start && start <= pars[startpit].size());
311         BOOST_ASSERT(0 <= end && end <= pars[endpit].size());
312         BOOST_ASSERT(startpit != endpit || start <= end);
313
314         // Clone the paragraphs within the selection.
315         ParagraphList paragraphs(pars.begin() + startpit, pars.begin() + endpit + 1);
316         for_each(paragraphs.begin(), paragraphs.end(), resetOwnerAndChanges());
317
318         // Cut out the end of the last paragraph.
319         Paragraph & back = paragraphs.back();
320         back.erase(end, back.size());
321
322         // Cut out the begin of the first paragraph
323         Paragraph & front = paragraphs.front();
324         front.erase(0, start);
325
326         theCuts.push(make_pair(paragraphs, tc));
327 }
328
329
330
331 PitPosPair cutSelectionHelper(BufferParams const & params,
332         ParagraphList & pars, pit_type startpit, pit_type endpit,
333         int startpos, int endpos, textclass_type tc, bool doclear)
334 {
335         copySelectionHelper(pars, startpit, endpit, startpos, endpos, tc);
336         return eraseSelectionHelper(params, pars, startpit, endpit,
337                 startpos, endpos, doclear);
338 }
339
340
341 } // namespace anon
342
343
344
345
346 namespace lyx {
347 namespace cap {
348
349 string grabAndEraseSelection(LCursor & cur)
350 {
351         if (!cur.selection())
352                 return string();
353         string res = grabSelection(cur);
354         eraseSelection(cur);
355         cur.selection() = false;
356         return res;
357 }
358
359
360 int SwitchLayoutsBetweenClasses(textclass_type c1, textclass_type c2,
361         ParagraphList & pars, ErrorList & errorlist)
362 {
363         BOOST_ASSERT(!pars.empty());
364         int ret = 0;
365         if (c1 == c2)
366                 return ret;
367
368         LyXTextClass const & tclass1 = textclasslist[c1];
369         LyXTextClass const & tclass2 = textclasslist[c2];
370
371         InsetText in;
372         std::swap(in.paragraphs(), pars);
373
374         ParIterator end = par_iterator_end(in);
375         for (ParIterator it = par_iterator_begin(in); it != end; ++it) {
376                 string const name = it->layout()->name();
377                 bool hasLayout = tclass2.hasLayout(name);
378
379                 if (hasLayout)
380                         it->layout(tclass2[name]);
381                 else
382                         it->layout(tclass2.defaultLayout());
383
384                 if (!hasLayout && name != tclass1.defaultLayoutName()) {
385                         ++ret;
386                         string const s = bformat(
387                                 _("Layout had to be changed from\n%1$s to %2$s\n"
388                                 "because of class conversion from\n%3$s to %4$s"),
389                          name, it->layout()->name(), tclass1.name(), tclass2.name());
390                         // To warn the user that something had to be done.
391                         errorlist.push_back(ErrorItem("Changed Layout", s,
392                                                       it->id(), 0,
393                                                       it->size()));
394                 }
395         }
396         std::swap(in.paragraphs(), pars);
397         return ret;
398 }
399
400
401 std::vector<string> const availableSelections(Buffer const & buffer)
402 {
403         vector<string> selList;
404
405         CutStack::const_iterator cit = theCuts.begin();
406         CutStack::const_iterator end = theCuts.end();
407         for (; cit != end; ++cit) {
408                 // we do not use cit-> here because gcc 2.9x does not
409                 // like it (JMarc)
410                 ParagraphList const & pars = (*cit).first;
411                 string asciiSel;
412                 ParagraphList::const_iterator pit = pars.begin();
413                 ParagraphList::const_iterator pend = pars.end();
414                 for (; pit != pend; ++pit) {
415                         asciiSel += pit->asString(buffer, false);
416                         if (asciiSel.size() > 25) {
417                                 asciiSel.replace(22, string::npos, "...");
418                                 break;
419                         }
420                 }
421
422                 selList.push_back(asciiSel);
423         }
424
425         return selList;
426 }
427
428
429 int nrOfParagraphs()
430 {
431         return theCuts.empty() ? 0 : theCuts[0].first.size();
432 }
433
434
435 void cutSelection(LCursor & cur, bool doclear, bool realcut)
436 {
437         if (cur.inTexted()) {
438                 LyXText * text = cur.text();
439                 BOOST_ASSERT(text);
440                 // Stuff what we got on the clipboard. Even if there is no selection.
441
442                 // There is a problem with having the stuffing here in that the
443                 // larger the selection the slower LyX will get. This can be
444                 // solved by running the line below only when the selection has
445                 // finished. The solution used currently just works, to make it
446                 // faster we need to be more clever and probably also have more
447                 // calls to stuffClipboard. (Lgb)
448                 cur.bv().stuffClipboard(cur.selectionAsString(true));
449
450                 // This doesn't make sense, if there is no selection
451                 if (!cur.selection())
452                         return;
453
454                 // OK, we have a selection. This is always between cur.selBegin()
455                 // and cur.selEnd()
456
457                 // make sure that the depth behind the selection are restored, too
458                 recordUndoSelection(cur);
459                 pit_type begpit = cur.selBegin().pit();
460                 pit_type endpit = cur.selEnd().pit();
461
462                 int endpos = cur.selEnd().pos();
463
464                 BufferParams const & bp = cur.buffer().params();
465                 if (realcut) {
466                         copySelectionHelper(text->paragraphs(),
467                                 begpit, endpit,
468                                 cur.selBegin().pos(), endpos,
469                                 bp.textclass);
470                 }
471
472                 boost::tie(endpit, endpos) =
473                         eraseSelectionHelper(bp,
474                                 text->paragraphs(),
475                                 begpit, endpit,
476                                 cur.selBegin().pos(), endpos,
477                                 doclear);
478
479                 // sometimes necessary
480                 if (doclear)
481                         text->paragraphs()[begpit].stripLeadingSpaces();
482
483                 // cutSelection can invalidate the cursor so we need to set
484                 // it anew. (Lgb)
485                 // we prefer the end for when tracking changes
486                 cur.pos() = endpos;
487                 cur.pit() = endpit;
488
489                 // need a valid cursor. (Lgb)
490                 cur.clearSelection();
491                 text->updateCounters();
492         }
493
494         if (cur.inMathed()) {
495                 lyxerr << "cutSelection in mathed" << endl;
496                 LCursor tmp = cur;
497                 copySelection(cur);
498                 cur.selection() = false;
499                 eraseSelection(tmp);
500         }
501 }
502
503
504 void copySelection(LCursor & cur)
505 {
506         // stuff the selection onto the X clipboard, from an explicit copy request
507         cur.bv().stuffClipboard(cur.selectionAsString(true));
508
509         // this doesn't make sense, if there is no selection
510         if (!cur.selection())
511                 return;
512
513         if (cur.inTexted()) {
514                 LyXText * text = cur.text();
515                 BOOST_ASSERT(text);
516                 // ok we have a selection. This is always between cur.selBegin()
517                 // and sel_end cursor
518
519                 // copy behind a space if there is one
520                 ParagraphList & pars = text->paragraphs();
521                 pos_type pos = cur.selBegin().pos();
522                 pit_type par = cur.selBegin().pit();
523                 while (pos < pars[par].size()
524                                          && pars[par].isLineSeparator(pos)
525                                          && (par != cur.selEnd().pit() || pos < cur.selEnd().pos()))
526                         ++pos;
527
528                 copySelectionHelper(pars, par, cur.selEnd().pit(),
529                         pos, cur.selEnd().pos(), cur.buffer().params().textclass);
530         }
531
532         if (cur.inMathed()) {
533                 lyxerr << "copySelection in mathed" << endl;
534                 ParagraphList pars;
535                 pars.push_back(Paragraph());
536                 BufferParams const & bp = cur.buffer().params();
537                 pars.back().layout(bp.getLyXTextClass().defaultLayout());
538                 for_each(pars.begin(), pars.end(), resetOwnerAndChanges());
539                 pars.back().insert(0, grabSelection(cur), LyXFont());
540                 theCuts.push(make_pair(pars, bp.textclass));
541         }
542 }
543
544
545 std::string getSelection(Buffer const & buf, size_t sel_index)
546 {
547         return sel_index < theCuts.size()
548                 ? theCuts[sel_index].first.back().asString(buf, false)
549                 : string();
550 }
551
552
553 void pasteSelection(LCursor & cur, size_t sel_index)
554 {
555         // this does not make sense, if there is nothing to paste
556         lyxerr << "#### pasteSelection " << sel_index << endl;
557         if (!checkPastePossible(sel_index))
558                 return;
559
560         if (cur.inTexted()) {
561                 LyXText * text = cur.text();
562                 BOOST_ASSERT(text);
563
564                 recordUndo(cur);
565
566                 pit_type endpit;
567                 PitPosPair ppp;
568
569                 ErrorList el;
570
571                 boost::tie(ppp, endpit) =
572                         pasteSelectionHelper(cur.buffer(),
573                                                                 text->paragraphs(),
574                                                                 cur.pit(), cur.pos(),
575                                                                 cur.buffer().params().textclass,
576                                                                 sel_index, el);
577                 bufferErrors(cur.buffer(), el);
578                 cur.bv().showErrorList(_("Paste"));
579
580                 cur.clearSelection();
581                 cur.resetAnchor();
582                 text->setCursor(cur, ppp.first, ppp.second);
583                 cur.setSelection();
584                 text->updateCounters();
585         }
586
587         if (cur.inMathed()) {
588                 lyxerr << "### should be handled in MathNest/GridInset" << endl;
589         }
590 }
591
592
593 void setSelectionRange(LCursor & cur, pos_type length)
594 {
595         LyXText * text = cur.text();
596         BOOST_ASSERT(text);
597         if (!length)
598                 return;
599         cur.resetAnchor();
600         while (length--)
601                 text->cursorRight(cur);
602         cur.setSelection();
603 }
604
605
606 // simple replacing. The font of the first selected character is used
607 void replaceSelectionWithString(LCursor & cur, string const & str)
608 {
609         LyXText * text = cur.text();
610         BOOST_ASSERT(text);
611         recordUndo(cur);
612
613         // Get font setting before we cut
614         pos_type pos = cur.selEnd().pos();
615         Paragraph & par = text->getPar(cur.selEnd().pit());
616         LyXFont const font =
617                 par.getFontSettings(cur.buffer().params(), cur.selBegin().pos());
618
619         // Insert the new string
620         string::const_iterator cit = str.begin();
621         string::const_iterator end = str.end();
622         for (; cit != end; ++cit, ++pos)
623                 par.insertChar(pos, (*cit), font);
624
625         // Cut the selection
626         cutSelection(cur, true, false);
627 }
628
629
630 void replaceSelection(LCursor & cur)
631 {
632         if (cur.selection())
633                 cutSelection(cur, true, false);
634 }
635
636
637 // only used by the spellchecker
638 void replaceWord(LCursor & cur, string const & replacestring)
639 {
640         LyXText * text = cur.text();
641         BOOST_ASSERT(text);
642
643         replaceSelectionWithString(cur, replacestring);
644         setSelectionRange(cur, replacestring.length());
645
646         // Go back so that replacement string is also spellchecked
647         for (string::size_type i = 0; i < replacestring.length() + 1; ++i)
648                 text->cursorLeft(cur);
649 }
650
651
652 void eraseSelection(LCursor & cur)
653 {
654         //lyxerr << "LCursor::eraseSelection begin: " << cur << endl;
655         CursorSlice const & i1 = cur.selBegin();
656         CursorSlice const & i2 = cur.selEnd();
657         if (i1.inset().asMathInset()) {
658                 if (i1.idx() == i2.idx()) {
659                         i1.cell().erase(i1.pos(), i2.pos());
660                 } else {
661                         MathInset * p = i1.asMathInset();
662                         InsetBase::row_type r1, r2;
663                         InsetBase::col_type c1, c2;
664                         region(i1, i2, r1, r2, c1, c2);
665                         for (InsetBase::row_type row = r1; row <= r2; ++row)
666                                 for (InsetBase::col_type col = c1; col <= c2; ++col)
667                                         p->cell(p->index(row, col)).clear();
668                 }
669                 cur.back() = i1;
670                 cur.pos() = 0; // We've deleted the whole cell. Only pos 0 is valid.
671                 cur.resetAnchor();
672         } else {
673                 lyxerr << "can't erase this selection 1" << endl;
674         }
675         //lyxerr << "LCursor::eraseSelection end: " << cur << endl;
676 }
677
678
679 void selDel(LCursor & cur)
680 {
681         //lyxerr << "LCursor::selDel" << endl;
682         if (cur.selection()) {
683                 eraseSelection(cur);
684                 cur.selection() = false;
685         }
686 }
687
688
689 void selClearOrDel(LCursor & cur)
690 {
691         //lyxerr << "LCursor::selClearOrDel" << endl;
692         if (lyxrc.auto_region_delete)
693                 selDel(cur);
694         else
695                 cur.selection() = false;
696 }
697
698
699 string grabSelection(LCursor & cur)
700 {
701         if (!cur.selection())
702                 return string();
703
704         CursorSlice i1 = cur.selBegin();
705         CursorSlice i2 = cur.selEnd();
706
707         if (i1.idx() == i2.idx()) {
708                 if (i1.inset().asMathInset()) {
709                         MathArray::const_iterator it = i1.cell().begin();
710                         return asString(MathArray(it + i1.pos(), it + i2.pos()));
711                 } else {
712                         return "unknown selection 1";
713                 }
714         }
715
716         InsetBase::row_type r1, r2;
717         InsetBase::col_type c1, c2;
718         region(i1, i2, r1, r2, c1, c2);
719
720         string data;
721         if (i1.inset().asMathInset()) {
722                 for (InsetBase::row_type row = r1; row <= r2; ++row) {
723                         if (row > r1)
724                                 data += "\\\\";
725                         for (InsetBase::col_type col = c1; col <= c2; ++col) {
726                                 if (col > c1)
727                                         data += '&';
728                                 data += asString(i1.asMathInset()->
729                                         cell(i1.asMathInset()->index(row, col)));
730                         }
731                 }
732         } else {
733                 data = "unknown selection 2";
734         }
735         return data;
736 }
737
738
739 } // namespace cap
740 } // namespace lyx