1 /* This file is part of the KDE project
2    Copyright (C) 2006 Martin Pfeiffer <hubipete@gmx.net>
3                  2009 Jeremias Epperlein <jeeree@web.de>
4 
5    This library is free software; you can redistribute it and/or
6    modify it under the terms of the GNU Library General Public
7    License as published by the Free Software Foundation; either
8    version 2 of the License, or (at your option) any later version.
9 
10    This library is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13    Library General Public License for more details.
14 
15    You should have received a copy of the GNU Library General Public License
16    along with this library; see the file COPYING.LIB.  If not, write to
17    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18    Boston, MA 02110-1301, USA.
19  */
20 
21 #include "KoFormulaTool.h"
22 
23 #include "KoFormulaShape.h"
24 #include "FormulaToolWidget.h"
25 #include "BasicElement.h"
26 #include "FormulaEditor.h"
27 #include "FormulaDebug.h"
28 
29 #include <KoCanvasBase.h>
30 #include <KoPointerEvent.h>
31 #include <KoSelection.h>
32 #include <KoShapeController.h>
33 #include <KoIcon.h>
34 
35 #include <klocalizedstring.h>
36 
37 #include <QKeyEvent>
38 #include <QAction>
39 #include <QPainter>
40 #include <QFile>
41 #include <QFileDialog>
42 
43 #include <KoShapeSavingContext.h>
44 #include <KoShapeLoadingContext.h>
45 #include <KoOdfLoadingContext.h>
46 #include <KoOdfStylesReader.h>
47 #include "FormulaCommand.h"
48 #include "FormulaCommandUpdate.h"
49 #include <KoXmlReader.h>
50 #include <KoXmlWriter.h>
51 #include <KoGenStyles.h>
52 #include <KoEmbeddedDocumentSaver.h>
53 #include "FormulaRenderer.h"
54 #include <QClipboard>
55 
56 // this adds const to non-const objects (like std::as_const)
koAsConst(T & t)57 template <typename T> Q_DECL_CONSTEXPR typename std::add_const<T>::type &koAsConst(T &t) noexcept { return t; }
58 // prevent rvalue arguments:
59 template <typename T> void koAsConst(const T &&) = delete;
60 
61 
KoFormulaTool(KoCanvasBase * canvas)62 KoFormulaTool::KoFormulaTool( KoCanvasBase* canvas ) : KoToolBase( canvas ),
63                                                        m_formulaShape( 0 ),
64                                                        m_formulaEditor( 0 )
65 {
66     setupActions();
67     setTextMode(true);
68 }
69 
~KoFormulaTool()70 KoFormulaTool::~KoFormulaTool()
71 {
72     if( m_formulaEditor ) {
73         m_cursorList.removeAll(m_formulaEditor);
74         delete m_formulaEditor;
75     }
76     foreach (FormulaEditor* tmp, m_cursorList) {
77         delete tmp;
78     }
79 }
80 
activate(ToolActivation toolActivation,const QSet<KoShape * > & shapes)81 void KoFormulaTool::activate(ToolActivation toolActivation, const QSet<KoShape*> &shapes)
82 {
83     Q_UNUSED(toolActivation);
84     foreach (KoShape *shape, shapes) {
85         m_formulaShape = dynamic_cast<KoFormulaShape*>( shape );
86         if( m_formulaShape )
87             break;
88     }
89 
90     if( m_formulaShape == 0 )  // none found
91     {
92         emit done();
93         return;
94     }
95     useCursor(Qt::IBeamCursor);
96     m_formulaEditor=0;
97     for (int i = 0; i < m_cursorList.count(); i++) {
98         FormulaEditor* editor = m_cursorList[i];
99         FormulaData* formulaData=m_formulaShape->formulaData();
100         if (editor->formulaData() == formulaData) {
101             //we have to check if the cursors current element is actually a
102             //child of the m_formulaShape->formulaData()
103             m_cursorList.removeAll(editor);
104             if (formulaData->formulaElement()->hasDescendant(editor->cursor().currentElement())) {
105                 if (editor->cursor().isAccepted()) {
106                     debugFormula << "Found old cursor";
107                     m_formulaEditor=editor;
108                     break;
109                 }
110             }
111             delete editor;
112         }
113     }
114     if (m_formulaEditor==0) {
115         //TODO: there should be a extra constructor for this
116         m_formulaEditor = new FormulaEditor( m_formulaShape->formulaData());
117     }
118     connect(m_formulaShape->formulaData(), SIGNAL(dataChanged(FormulaCommand*,bool)), this, SLOT(updateCursor(FormulaCommand*,bool)));
119     for (const TemplateAction &templateAction : koAsConst(m_templateActions)) {
120         connect(templateAction.action, &QAction::triggered, this, [this, templateAction] { insert(templateAction.data); });
121     }
122     //Only for debugging:
123     connect(action("write_elementTree"),SIGNAL(triggered(bool)), m_formulaShape->formulaData(), SLOT(writeElementTree()));
124 }
125 
deactivate()126 void KoFormulaTool::deactivate()
127 {
128     for (const TemplateAction &templateAction : koAsConst(m_templateActions)) {
129         disconnect(templateAction.action, &QAction::triggered, this, nullptr);
130     }
131     disconnect(m_formulaShape->formulaData(),0,this,0);
132     if (canvas()) {
133         m_cursorList.append(m_formulaEditor);
134         debugFormula << "Appending cursor";
135     }
136     if (m_cursorList.count() > 20) { // don't let it grow indefinitely
137         delete m_cursorList.takeAt(0);
138     }
139     m_formulaShape = 0;
140 }
141 
142 
updateCursor(FormulaCommand * command,bool undo)143 void KoFormulaTool::updateCursor(FormulaCommand* command, bool undo)
144 {
145     if (command!=0) {
146         debugFormula << "Going to change cursor";
147         command->changeCursor(m_formulaEditor->cursor(),undo);
148     } else {
149         debugFormula << "Going to reset cursor";
150         resetFormulaEditor();
151     }
152     repaintCursor();
153 }
154 
155 
paint(QPainter & painter,const KoViewConverter & converter)156 void KoFormulaTool::paint( QPainter &painter, const KoViewConverter &converter )
157 {
158     painter.save();
159     // transform painter from view coordinate system to document coordinate system
160     // remember that matrix multiplication is not commutative so painter.matrix
161     // has to come last
162     painter.setTransform(m_formulaShape->absoluteTransformation(&converter) * painter.transform());
163     KoShape::applyConversion(painter,converter);
164     m_formulaShape->formulaRenderer()->paintElement(painter,m_formulaShape->formulaData()->formulaElement(),true);
165     m_formulaEditor->paint( painter );
166     painter.restore();
167 }
168 
repaintCursor()169 void KoFormulaTool::repaintCursor()
170 {
171     canvas()->updateCanvas( m_formulaShape->boundingRect() );
172 }
173 
mousePressEvent(KoPointerEvent * event)174 void KoFormulaTool::mousePressEvent( KoPointerEvent *event )
175 {
176     // Check if the event is valid means inside the shape
177     if(!m_formulaShape->boundingRect().contains( event->point )) {
178         return;
179     }
180     // transform the global coordinates into shape coordinates
181     QPointF p = m_formulaShape->absoluteTransformation(0).inverted().map( event->point );
182     if (event->modifiers() & Qt::ShiftModifier) {
183         m_formulaEditor->cursor().setSelecting(true);
184     } else {
185         m_formulaEditor->cursor().setSelecting(false);
186     }
187     // set the cursor to the element the user clicked on
188     m_formulaEditor->cursor().setCursorTo( p );
189 
190     repaintCursor();
191     event->accept();
192 }
193 
mouseDoubleClickEvent(KoPointerEvent * event)194 void KoFormulaTool::mouseDoubleClickEvent( KoPointerEvent *event )
195 {
196     if( !m_formulaShape->boundingRect().contains( event->point ) ) {
197         return;
198     }
199     // transform the global coordinates into shape coordinates
200     QPointF p = m_formulaShape->absoluteTransformation(0).inverted().map( event->point );
201 
202     //clear the current selection
203     m_formulaEditor->cursor().setSelecting(false);
204     //place the cursor
205     m_formulaEditor->cursor().setCursorTo(p);
206     m_formulaEditor->cursor().selectElement(m_formulaEditor->cursor().currentElement());
207     repaintCursor();
208     event->accept();
209 }
210 
mouseMoveEvent(KoPointerEvent * event)211 void KoFormulaTool::mouseMoveEvent( KoPointerEvent *event )
212 {
213 //     Q_UNUSED( event )
214     if (!(event->buttons() & Qt::LeftButton)) {
215 	return;
216     }
217     // Check if the event is valid means inside the shape
218     if( !m_formulaShape->boundingRect().contains( event->point ) )
219         debugFormula << "Getting most probably invalid mouseMoveEvent";
220 
221     // transform the global coordinates into shape coordinates
222     QPointF p = m_formulaShape->absoluteTransformation(0).inverted().map( event->point );
223     //TODO Implement drag and drop of elements
224     m_formulaEditor->cursor().setSelecting(true);
225     m_formulaEditor->cursor().setCursorTo( p );
226     repaintCursor();
227     event->accept();
228 }
229 
mouseReleaseEvent(KoPointerEvent * event)230 void KoFormulaTool::mouseReleaseEvent( KoPointerEvent *event )
231 {
232     Q_UNUSED( event )
233 
234     // TODO Implement drag and drop
235 }
236 
keyPressEvent(QKeyEvent * event)237 void KoFormulaTool::keyPressEvent( QKeyEvent *event )
238 {
239     FormulaCommand *command=0;
240     if( !m_formulaEditor )
241         return;
242 
243     if (event->key() == Qt::Key_Left || event->key() == Qt::Key_Right ||
244         event->key() == Qt::Key_Up || event->key() == Qt::Key_Down ||
245         event->key() == Qt::Key_Home || event->key() == Qt::Key_End ) {
246         if (event->modifiers() & Qt::ShiftModifier) {
247             m_formulaEditor->cursor().setSelecting(true);
248         } else {
249             m_formulaEditor->cursor().setSelecting(false);
250         }
251     }
252     switch( event->key() )                           // map key to movement or action
253     {
254         case Qt::Key_Backspace:
255             m_formulaShape->update();
256             command=m_formulaEditor->remove( true );
257             m_formulaShape->updateLayout();
258             m_formulaShape->update();
259             break;
260         case Qt::Key_Delete:
261             m_formulaShape->update();
262             command=m_formulaEditor->remove( false );
263             m_formulaShape->updateLayout();
264             m_formulaShape->update();
265             break;
266         case Qt::Key_Left:
267             m_formulaEditor->cursor().move( MoveLeft );
268             break;
269         case Qt::Key_Up:
270             m_formulaEditor->cursor().move( MoveUp );
271             break;
272         case Qt::Key_Right:
273             m_formulaEditor->cursor().move( MoveRight );
274             break;
275         case Qt::Key_Down:
276             m_formulaEditor->cursor().move( MoveDown );
277             break;
278         case Qt::Key_End:
279             m_formulaEditor->cursor().moveEnd();
280             break;
281         case Qt::Key_Home:
282             m_formulaEditor->cursor().moveHome();
283             break;
284         default:
285             if( event->text().length() != 0 ) {
286                 command=m_formulaEditor->insertText( event->text() );
287             }
288     }
289     if (command!=0) {
290         canvas()->addCommand(new FormulaCommandUpdate(m_formulaShape,command));
291     }
292     repaintCursor();
293     event->accept();
294 }
295 
keyReleaseEvent(QKeyEvent * event)296 void KoFormulaTool::keyReleaseEvent( QKeyEvent *event )
297 {
298     event->accept();
299 }
300 
remove(bool backSpace)301 void KoFormulaTool::remove( bool backSpace )
302 {
303     m_formulaShape->update();
304     m_formulaEditor->remove( backSpace );
305     m_formulaShape->updateLayout();
306     m_formulaShape->update();
307 }
308 
insert(const QString & action)309 void KoFormulaTool::insert( const QString& action )
310 {
311     FormulaCommand *command;
312     m_formulaShape->update();
313     command=m_formulaEditor->insertMathML( action );
314     if (command!=0) {
315         canvas()->addCommand(new FormulaCommandUpdate(m_formulaShape, command));
316     }
317 }
318 
changeTable(QAction * action)319 void KoFormulaTool::changeTable ( QAction* action )
320 {
321     FormulaCommand *command;
322     m_formulaShape->update();
323     bool row=action->data().toList()[0].toBool();
324     bool insert=action->data().toList()[1].toBool();
325     command=m_formulaEditor->changeTable(insert,row);
326     if (command!=0) {
327         canvas()->addCommand(new FormulaCommandUpdate(m_formulaShape, command));
328     }
329 }
330 
insertSymbol(const QString & symbol)331 void KoFormulaTool::insertSymbol ( const QString& symbol )
332 {
333     FormulaCommand *command;
334     m_formulaShape->update();
335     command=m_formulaEditor->insertText( symbol );
336     if (command!=0) {
337         canvas()->addCommand(new FormulaCommandUpdate(m_formulaShape, command));
338     }
339 }
340 
341 
createOptionWidget()342 QWidget* KoFormulaTool::createOptionWidget()
343 {
344     FormulaToolWidget* options = new FormulaToolWidget( this );
345     options->setFormulaTool( this );
346     return options;
347 }
348 
shape()349 KoFormulaShape* KoFormulaTool::shape()
350 {
351     return m_formulaShape;
352 }
353 
354 
formulaEditor()355 FormulaEditor* KoFormulaTool::formulaEditor()
356 {
357     return m_formulaEditor;
358 }
359 
360 
resetFormulaEditor()361 void KoFormulaTool::resetFormulaEditor() {
362     m_formulaEditor->setData(m_formulaShape->formulaData());
363     FormulaCursor cursor(FormulaCursor(m_formulaShape->formulaData()->formulaElement(),false,0,0));
364     m_formulaEditor->setCursor(cursor);
365     //if the cursor is not allowed at the beginning of the formula, move it right
366     //TODO: check, if this can ever happen
367     if ( !m_formulaEditor->cursor().isAccepted() ) {
368         m_formulaEditor->cursor().move(MoveRight);
369     }
370 }
371 
loadFormula()372 void KoFormulaTool::loadFormula()
373 {
374     // get an filepath
375     const QString fileName = QFileDialog::getOpenFileName();
376     if( fileName.isEmpty() || !shape() )
377         return;
378 
379     // open the file the filepath points to
380     QFile file( fileName );
381     if( !file.open( QIODevice::ReadOnly | QIODevice::Text ) )
382         return;
383 
384     KoOdfStylesReader stylesReader;
385     KoOdfLoadingContext odfContext( stylesReader, 0 );
386     KoShapeLoadingContext shapeContext(odfContext, canvas()->shapeController()->resourceManager());
387 
388     // setup a DOM structure and start the actual loading process
389     KoXmlDocument tmpDocument;
390     tmpDocument.setContent( &file, false, 0, 0, 0 );
391     FormulaElement* formulaElement = new FormulaElement();     // create a new root element
392     formulaElement->readMathML( tmpDocument.documentElement() );     // and load the new formula
393     FormulaCommand* command=new FormulaCommandLoad(m_formulaShape->formulaData(),formulaElement);
394     canvas()->addCommand(new FormulaCommandUpdate(m_formulaShape, command));
395 }
396 
saveFormula()397 void KoFormulaTool::saveFormula()
398 {
399     const QString filePath = QFileDialog::getSaveFileName();
400     if( filePath.isEmpty() || !shape() )
401         return;
402 
403     QFile file( filePath );
404     KoXmlWriter writer( &file );
405     KoGenStyles styles;
406     KoEmbeddedDocumentSaver embeddedSaver;
407     KoShapeSavingContext shapeSavingContext( writer, styles, embeddedSaver );
408 
409     m_formulaShape->formulaData()->saveMathML( shapeSavingContext );
410 }
411 
setupActions()412 void KoFormulaTool::setupActions()
413 {
414     //notice that only empty mrows hows parent is a inferred mrow are treated as placeholders
415     //this causes the <mrow><mrow/></mrow> constructs
416     addTemplateAction(i18n("Insert fenced element"), "insert_fence","<mfenced><mrow/></mfenced>", koIconNameCStr("brackets"));
417     addTemplateAction(i18n("Insert enclosed element"), "insert_enclosed",
418                       "<menclosed><mrow/></menclosed>", koIconNameCStr("enclosed"));
419 
420     addTemplateAction(i18n("Insert root"), "insert_root","<mroot><mrow><mrow/></mrow></mroot>", koIconNameCStr("root"));
421     addTemplateAction(i18n("Insert square root"), "insert_sqrt","<msqrt><mrow/></msqrt>", koIconNameCStr("sqrt"));
422 
423     addTemplateAction(i18n("Insert fraction"), "insert_fraction",
424                       "<mfrac><mrow><mrow/></mrow><mrow/></mfrac>", koIconNameCStr("frac"));
425     addTemplateAction(i18n("Insert bevelled fraction"), "insert_bevelled_fraction",
426                       "<mfrac bevelled=\"true\"><mrow><mrow/></mrow><mrow/></mfrac>", koIconNameCStr("bevelled"));
427 
428     addTemplateAction(i18n("Insert 3x3 table"), "insert_33table",
429                       "<mtable><mtr><mtd><mrow /></mtd><mtd></mtd><mtd></mtd></mtr>" \
430                       "<mtr><mtd></mtd><mtd></mtd><mtd></mtd></mtr>" \
431                       "<mtr><mtd></mtd><mtd></mtd><mtd></mtd></mtr></mtable>", koIconNameCStr("matrix"));
432     addTemplateAction(i18n("Insert 2 dimensional vector"), "insert_21table",
433                       "<mtable><mtr><mtd><mrow/></mtd></mtr><mtr><mtd></mtd></mtr></mtable>", koIconNameCStr("vector"));
434 
435     addTemplateAction(i18n("Insert subscript"), "insert_subscript",
436                       "<msub><mrow><mrow/></mrow><mrow/></msubsup>", koIconNameCStr("rsub"));
437     addTemplateAction(i18n("Insert superscript"), "insert_supscript",
438                       "<msup><mrow><mrow/></mrow><mrow/></msup>", koIconNameCStr("rsup"));
439     addTemplateAction(i18n("Insert sub- and superscript"), "insert_subsupscript",
440                       "<msubsup><mrow><mrow/></mrow><mrow/><mrow/></msubsup>", koIconNameCStr("rsubup"));
441     addTemplateAction(i18n("Insert overscript"), "insert_overscript",
442                       "<mover><mrow><mrow/></mrow><mrow/></mover>", koIconNameCStr("gsup"));
443     addTemplateAction(i18n("Insert underscript"), "insert_underscript",
444                       "<munder><mrow><mrow/></mrow><mrow/></munder>", koIconNameCStr("gsub"));
445     addTemplateAction(i18n("Insert under- and overscript"), "insert_underoverscript",
446                       "<munderover><mrow><mrow/></mrow><mrow/><mrow/></munderover>", koIconNameCStr("gsubup"));
447 
448     //only for debugging
449     QAction * action;
450     action = new QAction( "Debug - writeElementTree" , this );
451     addAction( "write_elementTree", action );
452 
453     QList<QVariant> list;
454     action = new QAction( i18n( "Insert row" ), this );
455     list<<true<<true;
456     action->setData( list);
457     list.clear();
458     addAction( "insert_row", action );
459     action->setIcon(koIcon("insrow"));
460 
461     action = new QAction( i18n( "Insert column" ), this );
462     list<<false<<true;
463     action->setData( list);
464     list.clear();
465     addAction( "insert_column", action );
466     action->setIcon(koIcon("inscol"));
467 
468     action = new QAction( i18n( "Remove row" ), this );
469     list<<true<<false;
470     action->setData( list);
471     list.clear();
472     addAction( "remove_row", action );
473     action->setIcon(koIcon("remrow"));
474 
475     action = new QAction( i18n( "Remove column" ), this );
476     list<<false<<false;
477     action->setData( list);
478     list.clear();
479     addAction( "remove_column", action );
480     action->setIcon(koIcon("remcol"));
481 
482 }
483 
484 
addTemplateAction(const QString & caption,const QString & name,const QString & data,const char * iconName)485 void KoFormulaTool::addTemplateAction(const QString &caption, const QString &name, const QString &data,
486                                       const char *iconName)
487 {
488     QAction *action = new QAction( caption, this );
489     addAction(name , action);
490     action->setIcon(QIcon::fromTheme(QLatin1String(iconName)));
491     m_templateActions.push_back(TemplateAction { action, data });
492     // the connection takes place when this KoToolBase is activated
493 }
494 
495 
copy() const496 void KoFormulaTool::copy() const
497 {
498     QApplication::clipboard()->setText("test");
499 }
500 
deleteSelection()501 void KoFormulaTool::deleteSelection()
502 {
503     KoToolBase::deleteSelection();
504 }
505 
paste()506 bool KoFormulaTool::paste()
507 {
508     const QMimeData* data=QApplication::clipboard()->mimeData();
509     if (data->hasFormat("text/plain")) {
510         debugFormula << data->text();
511         FormulaCommand* command=m_formulaEditor->insertText(data->text());
512         if (command!=0) {
513             canvas()->addCommand(new FormulaCommandUpdate(m_formulaShape,command));
514         }
515         repaintCursor();
516         return true;
517     }
518     return false;
519 }
520 
supportedPasteMimeTypes() const521 QStringList KoFormulaTool::supportedPasteMimeTypes() const
522 {
523     QStringList tmp;
524     tmp << "text/plain";
525     tmp << "application/xml";
526     return tmp;
527 }
528