1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
4 *
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 *
9 * This file incorporates work covered by the following license notice:
10 *
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
18 */
19
20 #include <fusel.hxx>
21 #include <svx/svddrgmt.hxx>
22 #include <svx/svdpagv.hxx>
23 #include <svx/svdogrp.hxx>
24 #include <svx/scene3d.hxx>
25 #include <vcl/imapobj.hxx>
26 #include <unotools/securityoptions.hxx>
27 #include <svx/svxids.hrc>
28 #include <svx/xfillit0.hxx>
29 #include <svx/ImageMapInfo.hxx>
30 #include <sfx2/viewfrm.hxx>
31 #include <svl/stritem.hxx>
32 #include <svl/intitem.hxx>
33 #include <sfx2/dispatch.hxx>
34 #include <sfx2/docfile.hxx>
35 #include <editeng/flditem.hxx>
36
37 #include <svx/svdotable.hxx>
38
39 #include <app.hrc>
40
41 #include <sdmod.hxx>
42 #include <DrawDocShell.hxx>
43 #include <stlpool.hxx>
44 #include <fudraw.hxx>
45 #include <ViewShell.hxx>
46 #include <ViewShellBase.hxx>
47 #include <FrameView.hxx>
48 #include <View.hxx>
49 #include <Window.hxx>
50 #include <drawdoc.hxx>
51 #include <DrawViewShell.hxx>
52 #include <ToolBarManager.hxx>
53 #include <Client.hxx>
54
55 #include <svx/svdundo.hxx>
56
57 #include <svx/sdrhittesthelper.hxx>
58
59 #include <LibreOfficeKit/LibreOfficeKitEnums.h>
60 #include <comphelper/lok.hxx>
61
62 using namespace ::com::sun::star;
63
64 namespace sd {
65
FuSelection(ViewShell * pViewSh,::sd::Window * pWin,::sd::View * pView,SdDrawDocument * pDoc,SfxRequest & rReq)66 FuSelection::FuSelection (
67 ViewShell* pViewSh,
68 ::sd::Window* pWin,
69 ::sd::View* pView,
70 SdDrawDocument* pDoc,
71 SfxRequest& rReq)
72 : FuDraw(pViewSh, pWin, pView, pDoc, rReq),
73 bTempRotation(false),
74 bSelectionChanged(false),
75 pHdl(nullptr),
76 bSuppressChangesOfSelection(false),
77 bMirrorSide0(false),
78 nEditMode(SID_BEZIER_MOVE),
79 pWaterCanCandidate(nullptr)
80 //Add Shift+UP/DOWN/LEFT/RIGHT key to move the position of insert point,
81 //and SHIFT+ENTER key to decide the position and draw the new insert point
82 ,bBeginInsertPoint(false),
83 oldPoint(0,0)
84 ,bMovedToCenterPoint(false)
85 {
86 }
87
Create(ViewShell * pViewSh,::sd::Window * pWin,::sd::View * pView,SdDrawDocument * pDoc,SfxRequest & rReq)88 rtl::Reference<FuPoor> FuSelection::Create( ViewShell* pViewSh, ::sd::Window* pWin, ::sd::View* pView, SdDrawDocument* pDoc, SfxRequest& rReq )
89 {
90 rtl::Reference<FuPoor> xFunc( new FuSelection( pViewSh, pWin, pView, pDoc, rReq ) );
91 xFunc->DoExecute(rReq);
92 return xFunc;
93 }
94
DoExecute(SfxRequest & rReq)95 void FuSelection::DoExecute( SfxRequest& rReq )
96 {
97 FuDraw::DoExecute( rReq );
98
99 // Select object bar
100 SelectionHasChanged();
101 }
102
~FuSelection()103 FuSelection::~FuSelection()
104 {
105 mpView->UnmarkAllPoints();
106 mpView->ResetCreationActive();
107
108 if ( mpView->GetDragMode() != SdrDragMode::Move )
109 {
110 mpView->SetDragMode(SdrDragMode::Move);
111 }
112 }
113
114 namespace {
lcl_followHyperlinkAllowed(const MouseEvent & rMEvt)115 bool lcl_followHyperlinkAllowed(const MouseEvent& rMEvt) {
116 SvtSecurityOptions aSecOpt;
117 if (!rMEvt.IsMod1() && aSecOpt.IsOptionSet(SvtSecurityOptions::EOption::CtrlClickHyperlink))
118 return false;
119 if (rMEvt.IsMod1() && !aSecOpt.IsOptionSet(SvtSecurityOptions::EOption::CtrlClickHyperlink))
120 return false;
121 return true;
122 }
123 }
124
MouseButtonDown(const MouseEvent & rMEvt)125 bool FuSelection::MouseButtonDown(const MouseEvent& rMEvt)
126 {
127 pHdl = nullptr;
128 bool bReturn = FuDraw::MouseButtonDown(rMEvt);
129 bool bWaterCan = SD_MOD()->GetWaterCan();
130 const bool bReadOnly = mpDocSh->IsReadOnly();
131 // When the right mouse button is pressed then only select objects
132 // (and deselect others) as a preparation for showing the context
133 // menu.
134 const bool bSelectionOnly = rMEvt.IsRight();
135
136 bMBDown = true;
137 bSelectionChanged = false;
138
139 if ( mpView->IsAction() )
140 {
141 if ( rMEvt.IsRight() )
142 mpView->BckAction();
143 return true;
144 }
145
146 sal_uInt16 nDrgLog = sal_uInt16 ( mpWindow->PixelToLogic(Size(DRGPIX,0)).Width() );
147 sal_uInt16 nHitLog = sal_uInt16 ( mpWindow->PixelToLogic(Size(HITPIX,0)).Width() );
148
149 if (comphelper::LibreOfficeKit::isActive())
150 {
151 // When tiled rendering, we always work in logic units, use the non-pixel constants.
152 nDrgLog = DRGLOG;
153 nHitLog = HITLOG;
154 }
155
156 // The following code is executed for right clicks as well as for left
157 // clicks in order to modify the selection for the right button as a
158 // preparation for the context menu. The functions BegMarkObject() and
159 // BegDragObject(), however, are not called for right clicks because a)
160 // it makes no sense and b) to have IsAction() return sal_False when called
161 // from Command() which is a prerequisite for the context menu.
162 if ((rMEvt.IsLeft() || rMEvt.IsRight())
163 && !mpView->IsAction()
164 && (mpView->IsFrameDragSingles() || !mpView->HasMarkablePoints()))
165 {
166 /******************************************************************
167 * NO BEZIER_EDITOR
168 ******************************************************************/
169 mpWindow->CaptureMouse();
170 pHdl = mpView->PickHandle(aMDPos);
171
172 Degree100 nAngle0 = GetAngle(aMDPos - mpView->GetRef1());
173 nAngle0 -= 27000_deg100;
174 nAngle0 = NormAngle36000(nAngle0);
175 bMirrorSide0 = nAngle0 < 18000_deg100;
176
177 if (!pHdl && mpView->Is3DRotationCreationActive())
178 {
179 /******************************************************************
180 * If 3D-rotation bodies are about to be created,
181 * end creation now.
182 ******************************************************************/
183 bSuppressChangesOfSelection = true;
184 mpWindow->EnterWait();
185 mpView->End3DCreation();
186 bSuppressChangesOfSelection = false;
187 mpView->ResetCreationActive();
188 mpWindow->LeaveWait();
189 }
190
191 bool bTextEdit = false;
192 SdrViewEvent aVEvt;
193 SdrHitKind eHit = mpView->PickAnything(rMEvt, SdrMouseEventKind::BUTTONDOWN, aVEvt);
194
195 if ( eHit == SdrHitKind::TextEditObj && ( mpViewShell->GetFrameView()->IsQuickEdit() || dynamic_cast< sdr::table::SdrTableObj* >( aVEvt.pObj ) != nullptr ) )
196 {
197 bTextEdit = true;
198 }
199
200 // When clicking into a URl field, also go to text edit mode (when not following the link)
201 if (!bTextEdit && eHit == SdrHitKind::UrlField && !rMEvt.IsMod2() && !lcl_followHyperlinkAllowed(rMEvt))
202 bTextEdit = true;
203
204 bool bPreventModify = mpDocSh->IsReadOnly();
205 if (bPreventModify && mpDocSh->GetSignPDFCertificate().is())
206 {
207 // If the just added signature line shape is selected, allow moving / resizing it.
208 bPreventModify = false;
209 }
210
211 if(!bTextEdit
212 && !bPreventModify
213 && ((mpView->IsMarkedHit(aMDPos, nHitLog) && !rMEvt.IsShift() && !rMEvt.IsMod2()) || pHdl != nullptr)
214 && (rMEvt.GetClicks() != 2)
215 )
216 {
217 if (!pHdl && mpView->Is3DRotationCreationActive())
218 {
219 // Switch between 3D-rotation body -> selection
220 mpView->ResetCreationActive();
221 }
222 else if (bWaterCan)
223 {
224 // Remember the selected object for proper handling in
225 // MouseButtonUp().
226 pWaterCanCandidate = pickObject (aMDPos);
227 }
228 else
229 {
230 // hit handle or marked object
231 bFirstMouseMove = true;
232 aDragTimer.Start();
233 }
234
235 if ( ! rMEvt.IsRight())
236 if (mpView->BegDragObj(aMDPos, nullptr, pHdl, nDrgLog))
237 mpView->GetDragMethod()->SetShiftPressed( rMEvt.IsShift() );
238 bReturn = true;
239 }
240 else
241 {
242 SdrPageView* pPV = nullptr;
243 SdrObject* pObj = !rMEvt.IsMod2() ? mpView->PickObj(aMDPos, mpView->getHitTolLog(), pPV, SdrSearchOptions::PICKMACRO) : nullptr;
244 if (pObj)
245 {
246 mpView->BegMacroObj(aMDPos, nHitLog, pObj, pPV, mpWindow);
247 bReturn = true;
248 }
249 else if ( bTextEdit )
250 {
251 sal_uInt16 nSdrObjKind = aVEvt.pObj->GetObjIdentifier();
252
253 if (aVEvt.pObj->GetObjInventor() == SdrInventor::Default &&
254 (nSdrObjKind == OBJ_TEXT ||
255 nSdrObjKind == OBJ_TITLETEXT ||
256 nSdrObjKind == OBJ_OUTLINETEXT ||
257 !aVEvt.pObj->IsEmptyPresObj()))
258 {
259 // Seamless Editing: branch to text input
260 if (!rMEvt.IsShift())
261 mpView->UnmarkAll();
262
263 SfxUInt16Item aItem(SID_TEXTEDIT, 1);
264 mpViewShell->GetViewFrame()->GetDispatcher()->
265 ExecuteList(SID_TEXTEDIT,
266 SfxCallMode::SYNCHRON | SfxCallMode::RECORD,
267 { &aItem });
268 return bReturn; // CAUTION, due to the synchronous slot the object is deleted now
269 }
270 }
271 else if ( !rMEvt.IsMod2() && rMEvt.GetClicks() == 1 &&
272 aVEvt.eEvent == SdrEventKind::ExecuteUrl )
273 {
274 mpWindow->ReleaseMouse();
275
276 // If tiled rendering, let client handles URL execution and early returns.
277 if (comphelper::LibreOfficeKit::isActive())
278 {
279 SfxViewShell& rSfxViewShell = mpViewShell->GetViewShellBase();
280 rSfxViewShell.libreOfficeKitViewCallback(LOK_CALLBACK_HYPERLINK_CLICKED, aVEvt.pURLField->GetURL().toUtf8().getStr());
281 return true;
282 }
283
284 if (!lcl_followHyperlinkAllowed(rMEvt))
285 return true;
286
287 SfxStringItem aStrItem(SID_FILE_NAME, aVEvt.pURLField->GetURL());
288 SfxStringItem aReferer(SID_REFERER, mpDocSh->GetMedium()->GetName());
289 SfxBoolItem aBrowseItem( SID_BROWSE, true );
290 SfxViewFrame* pFrame = mpViewShell->GetViewFrame();
291 mpWindow->ReleaseMouse();
292
293 if (rMEvt.IsMod1())
294 {
295 // Open in new frame
296 pFrame->GetDispatcher()->ExecuteList(SID_OPENDOC,
297 SfxCallMode::ASYNCHRON | SfxCallMode::RECORD,
298 { &aStrItem, &aBrowseItem, &aReferer });
299 }
300 else
301 {
302 // Open in current frame
303 SfxFrameItem aFrameItem(SID_DOCFRAME, pFrame);
304 pFrame->GetDispatcher()->ExecuteList(SID_OPENDOC,
305 SfxCallMode::ASYNCHRON | SfxCallMode::RECORD,
306 { &aStrItem, &aFrameItem, &aBrowseItem, &aReferer });
307 }
308
309 bReturn = true;
310 }
311 else if(!rMEvt.IsMod2()
312 && dynamic_cast< const DrawViewShell *>( mpViewShell ) != nullptr
313 )
314 {
315 pObj = mpView->PickObj(aMDPos, mpView->getHitTolLog(), pPV, SdrSearchOptions::ALSOONMASTER);
316 if (pObj)
317 {
318 // Handle ImageMap click when not just selecting
319 if (!bSelectionOnly)
320 {
321 if (lcl_followHyperlinkAllowed(rMEvt))
322 bReturn = HandleImageMapClick(pObj, aMDPos);
323 }
324
325 if (!bReturn
326 && (dynamic_cast<const SdrObjGroup*>(pObj) != nullptr
327 || dynamic_cast<const E3dScene*>(pObj) != nullptr))
328 {
329 if (rMEvt.GetClicks() == 1)
330 {
331 // Look into the group
332 pObj = mpView->PickObj(aMDPos, mpView->getHitTolLog(), pPV,
333 SdrSearchOptions::ALSOONMASTER
334 | SdrSearchOptions::DEEP);
335 if (pObj && lcl_followHyperlinkAllowed(rMEvt))
336 bReturn = HandleImageMapClick(pObj, aMDPos);
337 }
338 else if (!bReadOnly && rMEvt.GetClicks() == 2)
339 {
340 // New: double click on selected Group object
341 // enter group
342 if (!bSelectionOnly
343 && pObj->getSdrPageFromSdrObject() == pPV->GetPage())
344 bReturn = pPV->EnterGroup(pObj);
345 }
346 }
347 }
348
349 // #i71727# replaced else here with two possibilities, once the original else (!pObj)
350 // and also ignoring the found object when it's on a masterpage
351 if(!pObj || (pObj->getSdrPageFromSdrObject() && pObj->getSdrPageFromSdrObject()->IsMasterPage()))
352 {
353 if(mpView->IsGroupEntered() && 2 == rMEvt.GetClicks())
354 {
355 // New: double click on empty space/on obj on MasterPage, leave group
356 mpView->LeaveOneGroup();
357 bReturn = true;
358 }
359 }
360 }
361
362 if (!bReturn)
363 {
364 if (bWaterCan)
365 {
366 if ( ! (rMEvt.IsShift() || rMEvt.IsMod2()))
367 {
368 // Find the object under the current mouse position
369 // and store it for the MouseButtonUp() method to
370 // evaluate.
371 pWaterCanCandidate = pickObject (aMDPos);
372 }
373 }
374 else
375 {
376 bReturn = true;
377 bool bDeactivateOLE = false;
378
379 if ( !rMEvt.IsShift() && !rMEvt.IsMod2() )
380 {
381 OSL_ASSERT (mpViewShell->GetViewShell()!=nullptr);
382 Client* pIPClient = static_cast<Client*>(
383 mpViewShell->GetViewShell()->GetIPClient());
384
385 if (pIPClient && pIPClient->IsObjectInPlaceActive())
386 {
387 // OLE-object gets deactivated in subsequent UnmarkAll()
388 bDeactivateOLE = true;
389 }
390
391 mpView->UnmarkAll();
392 }
393
394 bool bMarked = false;
395
396 if ( !rMEvt.IsMod1() && !bDeactivateOLE)
397 {
398 if ( rMEvt.IsMod2() )
399 {
400 bMarked = mpView->MarkNextObj(aMDPos, nHitLog, rMEvt.IsShift() );
401 }
402 else
403 {
404 bool bToggle = false;
405
406 if (rMEvt.IsShift() && mpView->GetMarkedObjectList().GetMarkCount() > 1)
407 {
408 // No Toggle on single selection
409 bToggle = true;
410 }
411
412 bMarked = mpView->MarkObj(aMDPos, nHitLog, bToggle);
413 }
414 }
415
416 if( !bDeactivateOLE )
417 {
418 if ( !bReadOnly &&
419 bMarked &&
420 (!rMEvt.IsShift() || mpView->IsMarkedHit(aMDPos, nHitLog)))
421 {
422 /**********************************************************
423 * Move object
424 **********************************************************/
425 aDragTimer.Start();
426
427 pHdl=mpView->PickHandle(aMDPos);
428 if ( ! rMEvt.IsRight())
429 mpView->BegDragObj(aMDPos, nullptr, pHdl, nDrgLog);
430 }
431 else
432 {
433 /**********************************************************
434 * Select object
435 **********************************************************/
436 if ( ! rMEvt.IsRight())
437 mpView->BegMarkObj(aMDPos);
438 }
439 }
440
441 if( bMarked && bTempRotation && (nSlotId == SID_OBJECT_ROTATE) && !rMEvt.IsShift() && (rMEvt.GetClicks() != 2) )
442 {
443 nSlotId = SID_OBJECT_SELECT;
444 Activate();
445 }
446 }
447 }
448 }
449 }
450 else if ( !bReadOnly
451 && (rMEvt.IsLeft() || rMEvt.IsRight())
452 && !mpView->IsAction())
453 {
454 /**********************************************************************
455 * BEZIER-EDITOR
456 **********************************************************************/
457 mpWindow->CaptureMouse();
458 SdrViewEvent aVEvt;
459 SdrHitKind eHit = mpView->PickAnything(rMEvt, SdrMouseEventKind::BUTTONDOWN, aVEvt);
460
461 if (eHit == SdrHitKind::Handle && aVEvt.pHdl->GetKind() == SdrHdlKind::BezierWeight)
462 {
463 /******************************************************************
464 * Drag Handle
465 ******************************************************************/
466 if ( ! rMEvt.IsRight())
467 mpView->BegDragObj(aMDPos, nullptr, aVEvt.pHdl, nDrgLog);
468 }
469 else if (eHit == SdrHitKind::MarkedObject && nEditMode == SID_BEZIER_INSERT)
470 {
471 /******************************************************************
472 * Insert glue point
473 ******************************************************************/
474 mpView->BegInsObjPoint(aMDPos, rMEvt.IsMod1());
475 }
476 else if (eHit == SdrHitKind::MarkedObject && rMEvt.IsMod1())
477 {
478 /******************************************************************
479 * Select glue point
480 ******************************************************************/
481 if (!rMEvt.IsShift())
482 mpView->UnmarkAllPoints();
483
484 if ( ! rMEvt.IsRight())
485 mpView->BegMarkPoints(aMDPos);
486 }
487 else if (eHit == SdrHitKind::MarkedObject && !rMEvt.IsShift() && !rMEvt.IsMod2())
488 {
489 /******************************************************************
490 * Move object
491 ******************************************************************/
492 if ( ! rMEvt.IsRight())
493 mpView->BegDragObj(aMDPos, nullptr, nullptr, nDrgLog);
494 }
495 else if (eHit == SdrHitKind::Handle)
496 {
497 /******************************************************************
498 * Select glue point
499 ******************************************************************/
500 if (!mpView->IsPointMarked(*aVEvt.pHdl) || rMEvt.IsShift())
501 {
502 if (!rMEvt.IsShift())
503 {
504 mpView->UnmarkAllPoints();
505 pHdl = mpView->PickHandle(aMDPos);
506 }
507 else
508 {
509 if (mpView->IsPointMarked(*aVEvt.pHdl))
510 {
511 mpView->UnmarkPoint(*aVEvt.pHdl);
512 pHdl = nullptr;
513 }
514 else
515 {
516 pHdl = mpView->PickHandle(aMDPos);
517 }
518 }
519
520 if (pHdl)
521 {
522 mpView->MarkPoint(*pHdl);
523 if ( ! rMEvt.IsRight())
524 mpView->BegDragObj(aMDPos, nullptr, pHdl, nDrgLog);
525
526 }
527 }
528 else
529 {
530 // Point IS marked and NO shift is pressed. Start
531 // dragging of selected point(s)
532 pHdl = mpView->PickHandle(aMDPos);
533 if(pHdl && ! rMEvt.IsRight())
534 mpView->BegDragObj(aMDPos, nullptr, pHdl, nDrgLog);
535 }
536 }
537 else
538 {
539 /******************************************************************
540 * Select or drag object
541 ******************************************************************/
542 if (!rMEvt.IsShift() && !rMEvt.IsMod2() && eHit == SdrHitKind::UnmarkedObject)
543 {
544 mpView->UnmarkAllObj();
545 }
546
547 bool bMarked = false;
548
549 if (!rMEvt.IsMod1())
550 {
551 if (rMEvt.IsMod2())
552 {
553 bMarked = mpView->MarkNextObj(aMDPos, nHitLog, rMEvt.IsShift());
554 }
555 else
556 {
557 bMarked = mpView->MarkObj(aMDPos, nHitLog, rMEvt.IsShift());
558 }
559 }
560
561 if (bMarked &&
562 (!rMEvt.IsShift() || eHit == SdrHitKind::MarkedObject))
563 {
564 // Move object
565 if ( ! rMEvt.IsRight())
566 mpView->BegDragObj(aMDPos, nullptr, aVEvt.pHdl, nDrgLog);
567 }
568 else if (mpView->AreObjectsMarked())
569 {
570 /**************************************************************
571 * Select glue point
572 **************************************************************/
573 if (!rMEvt.IsShift())
574 mpView->UnmarkAllPoints();
575
576 if ( ! rMEvt.IsRight())
577 mpView->BegMarkPoints(aMDPos);
578 }
579 else
580 {
581 /**************************************************************
582 * Select object
583 **************************************************************/
584 if ( ! rMEvt.IsRight())
585 mpView->BegMarkObj(aMDPos);
586 }
587
588 ForcePointer(&rMEvt);
589 }
590 }
591
592 if (!bIsInDragMode)
593 {
594 ForcePointer(&rMEvt);
595 }
596
597 return bReturn;
598 }
599
MouseMove(const MouseEvent & rMEvt)600 bool FuSelection::MouseMove(const MouseEvent& rMEvt)
601 {
602 bool bReturn = FuDraw::MouseMove(rMEvt);
603
604 if (aDragTimer.IsActive())
605 {
606 if(bFirstMouseMove)
607 {
608 bFirstMouseMove = false;
609 }
610 else
611 {
612 aDragTimer.Stop();
613 }
614 }
615
616 if (mpView->IsAction())
617 {
618 Point aPix(rMEvt.GetPosPixel());
619 Point aPnt(mpWindow->PixelToLogic(aPix));
620
621 ForceScroll(aPix);
622
623 if (mpView->IsInsObjPoint())
624 {
625 mpView->MovInsObjPoint(aPnt);
626 }
627 else
628 {
629 mpView->MovAction(aPnt);
630 }
631 }
632
633 ForcePointer(&rMEvt);
634
635 return bReturn;
636 }
637
MouseButtonUp(const MouseEvent & rMEvt)638 bool FuSelection::MouseButtonUp(const MouseEvent& rMEvt)
639 {
640 bool bReturn = false;
641 // When the right mouse button is pressed then only select objects
642 // (and deselect others) as a preparation for showing the context
643 // menu.
644 const bool bSelectionOnly = rMEvt.IsRight();
645
646 if (aDragTimer.IsActive() )
647 {
648 aDragTimer.Stop();
649 bIsInDragMode = false;
650 }
651
652 if( !mpView )
653 return false;
654
655 Point aPnt( mpWindow->PixelToLogic( rMEvt.GetPosPixel() ) );
656 sal_uInt16 nHitLog = sal_uInt16 ( mpWindow->PixelToLogic(Size(HITPIX,0)).Width() );
657 sal_uInt16 nDrgLog = sal_uInt16 ( mpWindow->PixelToLogic(Size(DRGPIX,0)).Width() );
658
659 if (mpView->IsFrameDragSingles() || !mpView->HasMarkablePoints())
660 {
661 /**********************************************************************
662 * NO BEZIER_EDITOR
663 **********************************************************************/
664 if ( mpView->IsDragObj() )
665 {
666 /******************************************************************
667 * Object was moved
668 ******************************************************************/
669 FrameView* pFrameView = mpViewShell->GetFrameView();
670 bool bDragWithCopy = (rMEvt.IsMod1() && pFrameView->IsDragWithCopy());
671
672 if (bDragWithCopy)
673 {
674 bDragWithCopy = !mpView->IsPresObjSelected(false);
675 }
676
677 mpView->SetDragWithCopy(bDragWithCopy);
678 mpView->EndDragObj( mpView->IsDragWithCopy() );
679
680 mpView->ForceMarkedToAnotherPage();
681
682 if (!rMEvt.IsShift() && !rMEvt.IsMod1() && !rMEvt.IsMod2() &&
683 !bSelectionChanged &&
684 std::abs(aPnt.X() - aMDPos.X()) < nDrgLog &&
685 std::abs(aPnt.Y() - aMDPos.Y()) < nDrgLog)
686 {
687 /*************************************************************
688 * If a user wants to click on an object in front of a marked
689 * one, he releases the mouse button immediately
690 **************************************************************/
691 SdrPageView* pPV;
692 SdrObject* pObj = mpView->PickObj(aMDPos, mpView->getHitTolLog(), pPV, SdrSearchOptions::ALSOONMASTER | SdrSearchOptions::BEFOREMARK);
693 if (pObj && pPV->IsObjMarkable(pObj))
694 {
695 mpView->UnmarkAllObj();
696 mpView->MarkObj(pObj,pPV);
697 return true;
698 }
699 /**************************************************************
700 * Toggle between selection and rotation
701 **************************************************************/
702 SdrObject* pSingleObj = nullptr;
703
704 if (mpView->GetMarkedObjectList().GetMarkCount()==1)
705 {
706 pSingleObj = mpView->GetMarkedObjectList().GetMark(0)->GetMarkedSdrObj();
707 }
708
709 if (nSlotId == SID_OBJECT_SELECT
710 && !comphelper::LibreOfficeKit::isActive()
711 && mpView->IsRotateAllowed()
712
713 && (rMEvt.GetClicks() != 2)
714 && (mpViewShell->GetFrameView()->IsClickChangeRotation()
715 || (pSingleObj
716 && pSingleObj->GetObjInventor()==SdrInventor::E3d))
717 && ! bSelectionOnly)
718
719 {
720 bTempRotation = true;
721 nSlotId = SID_OBJECT_ROTATE;
722 Activate();
723 }
724 else if (nSlotId == SID_OBJECT_ROTATE)
725 {
726 nSlotId = SID_OBJECT_SELECT;
727 Activate();
728 }
729 }
730 else if (nSlotId == SID_CONVERT_TO_3D_LATHE)
731 {
732 if (!pHdl)
733 {
734 bSuppressChangesOfSelection = true;
735 mpView->Start3DCreation();
736 bSuppressChangesOfSelection = false;
737 }
738 else if (pHdl->GetKind() != SdrHdlKind::MirrorAxis &&
739 pHdl->GetKind() != SdrHdlKind::Ref1 &&
740 pHdl->GetKind() != SdrHdlKind::Ref2 && mpView->Is3DRotationCreationActive())
741 {
742 /*********************************************************
743 * If 3D-rotation bodies are about to be created,
744 * end creation now
745 **********************************************************/
746 Degree100 nAngle1 = GetAngle(aPnt - mpView->GetRef1());
747 nAngle1 -= 27000_deg100;
748 nAngle1 = NormAngle36000(nAngle1);
749 bool bMirrorSide1 = nAngle1 < 18000_deg100;
750
751 if (bMirrorSide0 != bMirrorSide1)
752 {
753 bSuppressChangesOfSelection = true;
754 mpWindow->EnterWait();
755 mpView->End3DCreation();
756 bSuppressChangesOfSelection = false;
757 nSlotId = SID_OBJECT_SELECT;
758 mpWindow->LeaveWait();
759 Activate();
760 }
761 }
762 }
763 }
764 else if (rMEvt.IsMod1()
765 && !rMEvt.IsMod2()
766 && std::abs(aPnt.X() - aMDPos.X()) < nDrgLog
767 && std::abs(aPnt.Y() - aMDPos.Y()) < nDrgLog)
768 {
769 // Enter group
770 mpView->MarkObj(aPnt, nHitLog, rMEvt.IsShift(), rMEvt.IsMod1());
771 }
772
773 if (mpView->IsAction() )
774 {
775 mpView->EndAction();
776 }
777
778 if( SD_MOD()->GetWaterCan() )
779 {
780 if( rMEvt.IsRight() )
781 {
782 // In watering-can mode, on press onto right mouse button, an undo is executed
783 mpViewShell->GetViewFrame()->GetDispatcher()->Execute( SID_UNDO, SfxCallMode::ASYNCHRON );
784 }
785 else if (pWaterCanCandidate != nullptr)
786 {
787 // Is the candidate object still under the mouse?
788 if (pickObject (aPnt) == pWaterCanCandidate)
789 {
790 SdStyleSheetPool* pPool = static_cast<SdStyleSheetPool*>(
791 mpDocSh->GetStyleSheetPool());
792 if (pPool != nullptr)
793 {
794 SfxStyleSheet* pStyleSheet = static_cast<SfxStyleSheet*>(
795 pPool->GetActualStyleSheet());
796 if (pStyleSheet != nullptr && mpView->IsUndoEnabled() )
797 {
798 // Added UNDOs for the WaterCan mode. This was never done in
799 // the past, thus it was missing all the time.
800 std::unique_ptr<SdrUndoAction> pUndoAttr = mpDoc->GetSdrUndoFactory().CreateUndoAttrObject(*pWaterCanCandidate, true, true);
801 mpView->BegUndo(pUndoAttr->GetComment());
802 mpView->AddUndo(mpDoc->GetSdrUndoFactory().CreateUndoGeoObject(*pWaterCanCandidate));
803 mpView->AddUndo(std::move(pUndoAttr));
804
805 pWaterCanCandidate->SetStyleSheet (pStyleSheet, false);
806
807 mpView->EndUndo();
808 }
809 }
810 }
811 }
812 // else when there has been no object under the mouse when the
813 // button was pressed then nothing happens even when there is
814 // one now.
815 }
816
817 sal_uInt16 nClicks = rMEvt.GetClicks();
818
819 if (nClicks == 2 && rMEvt.IsLeft() && bMBDown &&
820 !rMEvt.IsMod1() && !rMEvt.IsShift() )
821 {
822 DoubleClick(rMEvt);
823 }
824
825 bMBDown = false;
826
827 ForcePointer(&rMEvt);
828 pHdl = nullptr;
829 mpWindow->ReleaseMouse();
830 SdrObject* pSingleObj = nullptr;
831 const size_t nMarkCount = mpView->GetMarkedObjectList().GetMarkCount();
832
833 if (nMarkCount==1)
834 {
835 pSingleObj = mpView->GetMarkedObjectList().GetMark(0)->GetMarkedSdrObj();
836 }
837
838 if ( (nSlotId != SID_OBJECT_SELECT && nMarkCount==0) ||
839 ( mpView->GetDragMode() == SdrDragMode::Crook &&
840 !mpView->IsCrookAllowed( mpView->IsCrookNoContortion() ) ) ||
841 ( mpView->GetDragMode() == SdrDragMode::Shear &&
842 !mpView->IsShearAllowed() && !mpView->IsDistortAllowed() ) ||
843 ( nSlotId==SID_CONVERT_TO_3D_LATHE && pSingleObj &&
844 (pSingleObj->GetObjInventor() != SdrInventor::Default ||
845 pSingleObj->GetObjIdentifier() == OBJ_MEASURE) ) )
846 {
847 bReturn = true;
848 ForcePointer(&rMEvt);
849 pHdl = nullptr;
850 mpWindow->ReleaseMouse();
851 FuDraw::MouseButtonUp(rMEvt);
852 mpViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_OBJECT_SELECT, SfxCallMode::SYNCHRON);
853 return bReturn; // CAUTION, due to the synchronous slot, the object is deleted now.
854 }
855
856 FuDraw::MouseButtonUp(rMEvt);
857 }
858 else
859 {
860 /**********************************************************************
861 * BEZIER_EDITOR
862 **********************************************************************/
863 if ( mpView->IsAction() )
864 {
865 if ( mpView->IsInsObjPoint() )
866 {
867 mpView->EndInsObjPoint(SdrCreateCmd::ForceEnd);
868 }
869 else if ( mpView->IsDragObj() )
870 {
871 FrameView* pFrameView = mpViewShell->GetFrameView();
872 bool bDragWithCopy = (rMEvt.IsMod1() && pFrameView->IsDragWithCopy());
873
874 if (bDragWithCopy)
875 {
876 bDragWithCopy = !mpView->IsPresObjSelected(false);
877 }
878
879 mpView->SetDragWithCopy(bDragWithCopy);
880 mpView->EndDragObj( mpView->IsDragWithCopy() );
881 }
882 else
883 {
884 mpView->EndAction();
885
886 sal_uInt16 nDrgLog2 = sal_uInt16 ( mpWindow->PixelToLogic(Size(DRGPIX,0)).Width() );
887 Point aPos = mpWindow->PixelToLogic( rMEvt.GetPosPixel() );
888
889 if (std::abs(aMDPos.X() - aPos.X()) < nDrgLog2 &&
890 std::abs(aMDPos.Y() - aPos.Y()) < nDrgLog2 &&
891 !rMEvt.IsShift() && !rMEvt.IsMod2())
892 {
893 SdrViewEvent aVEvt;
894 SdrHitKind eHit = mpView->PickAnything(rMEvt, SdrMouseEventKind::BUTTONDOWN, aVEvt);
895
896 if (eHit == SdrHitKind::NONE)
897 {
898 // Click on the same place - unselect
899 mpView->UnmarkAllObj();
900 }
901 }
902 }
903 }
904 else if (!rMEvt.IsShift() && rMEvt.IsMod1() && !rMEvt.IsMod2() &&
905 std::abs(aPnt.X() - aMDPos.X()) < nDrgLog &&
906 std::abs(aPnt.Y() - aMDPos.Y()) < nDrgLog)
907 {
908 // Enter group
909 mpView->MarkObj(aPnt, nHitLog, false, rMEvt.IsMod1());
910 }
911
912 ForcePointer(&rMEvt);
913 pHdl = nullptr;
914 mpWindow->ReleaseMouse();
915
916 FuDraw::MouseButtonUp(rMEvt);
917 }
918
919 return bReturn;
920 }
921
922 /**
923 * Process keyboard input
924 * @returns sal_True if a KeyEvent is being processed, sal_False otherwise
925 */
KeyInput(const KeyEvent & rKEvt)926 bool FuSelection::KeyInput(const KeyEvent& rKEvt)
927 {
928 bool bReturn = false;
929
930 switch (rKEvt.GetKeyCode().GetCode())
931 {
932 case KEY_ESCAPE:
933 {
934 bReturn = FuSelection::cancel();
935 }
936 break;
937 //add keyboard operation for insert points in drawing curve
938 case KEY_UP:
939 case KEY_DOWN:
940 case KEY_LEFT:
941 case KEY_RIGHT:
942 {
943 if(rKEvt.GetKeyCode().IsShift()&&(nEditMode == SID_BEZIER_INSERT)){
944 ::tools::Long nX = 0;
945 ::tools::Long nY = 0;
946 sal_uInt16 nCode = rKEvt.GetKeyCode().GetCode();
947 if (nCode == KEY_UP)
948 {
949 // scroll up
950 nX = 0;
951 nY =-1;
952 }
953 else if (nCode == KEY_DOWN)
954 {
955 // scroll down
956 nX = 0;
957 nY = 1;
958 }
959 else if (nCode == KEY_LEFT)
960 {
961 // scroll left
962 nX =-1;
963 nY = 0;
964 }
965 else if (nCode == KEY_RIGHT)
966 {
967 // scroll right
968 nX = 1;
969 nY = 0;
970 }
971
972 Point centerPoint;
973 ::tools::Rectangle rect = mpView->GetMarkedObjRect();
974 centerPoint = mpWindow->LogicToPixel(rect.Center());
975 Point aPoint = bMovedToCenterPoint? oldPoint:centerPoint;
976 Point ePoint = aPoint + Point(nX,nY);
977 mpWindow->SetPointerPosPixel(ePoint);
978 //simulate mouse move action
979 MouseEvent eMevt(ePoint, 1, MouseEventModifiers::DRAGMOVE, MOUSE_LEFT, 0);
980 MouseMove(eMevt);
981 oldPoint = ePoint;
982 bMovedToCenterPoint = true;
983 bReturn = true;
984 }
985 }
986 break;
987 case KEY_RETURN:
988 if(rKEvt.GetKeyCode().IsShift()&&(nEditMode == SID_BEZIER_INSERT))
989 {
990 if(!bBeginInsertPoint)
991 {
992 //simulate mouse button down action
993 MouseEvent aMevt(oldPoint, 1,
994 MouseEventModifiers::SIMPLEMOVE | MouseEventModifiers::DRAGMOVE,
995 MOUSE_LEFT, KEY_SHIFT);
996 MouseButtonDown(aMevt);
997 mpWindow->CaptureMouse();
998 bBeginInsertPoint = true;
999 }
1000 else
1001 {
1002 //simulate mouse button up action
1003 MouseEvent rMEvt(oldPoint, 1,
1004 MouseEventModifiers::SIMPLEMOVE | MouseEventModifiers::ENTERWINDOW,
1005 MOUSE_LEFT, KEY_SHIFT);
1006 MouseButtonUp(rMEvt);
1007 bBeginInsertPoint = false;
1008 }
1009 bReturn= true;
1010 }
1011 break;
1012 }
1013 if (!bReturn)
1014 {
1015 bReturn = FuDraw::KeyInput(rKEvt);
1016
1017 if(mpView->GetMarkedObjectList().GetMarkCount() == 0)
1018 {
1019 mpView->ResetCreationActive();
1020
1021 mpViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_OBJECT_SELECT, SfxCallMode::ASYNCHRON | SfxCallMode::RECORD);
1022 }
1023 }
1024
1025 return bReturn;
1026
1027 }
1028
Activate()1029 void FuSelection::Activate()
1030 {
1031 SdrDragMode eMode;
1032 mpView->ResetCreationActive();
1033 mpView->SetEditMode(SdrViewEditMode::Edit);
1034
1035 switch( nSlotId )
1036 {
1037 case SID_OBJECT_ROTATE:
1038 {
1039 eMode = SdrDragMode::Rotate;
1040
1041 if ( mpView->GetDragMode() != eMode )
1042 mpView->SetDragMode(eMode);
1043 }
1044 break;
1045
1046 case SID_OBJECT_MIRROR:
1047 {
1048 eMode = SdrDragMode::Mirror;
1049
1050 if ( mpView->GetDragMode() != eMode )
1051 mpView->SetDragMode(eMode);
1052 }
1053 break;
1054
1055 case SID_OBJECT_CROP:
1056 {
1057 eMode = SdrDragMode::Crop;
1058
1059 if ( mpView->GetDragMode() != eMode )
1060 mpView->SetDragMode(eMode);
1061 }
1062 break;
1063
1064 case SID_OBJECT_TRANSPARENCE:
1065 {
1066 eMode = SdrDragMode::Transparence;
1067
1068 if ( mpView->GetDragMode() != eMode )
1069 mpView->SetDragMode(eMode);
1070 }
1071 break;
1072
1073 case SID_OBJECT_GRADIENT:
1074 {
1075 eMode = SdrDragMode::Gradient;
1076
1077 if ( mpView->GetDragMode() != eMode )
1078 mpView->SetDragMode(eMode);
1079 }
1080 break;
1081
1082 case SID_OBJECT_SHEAR:
1083 {
1084 eMode = SdrDragMode::Shear;
1085
1086 if ( mpView->GetDragMode() != eMode )
1087 mpView->SetDragMode(eMode);
1088 }
1089 break;
1090
1091 case SID_OBJECT_CROOK_ROTATE:
1092 {
1093 eMode = SdrDragMode::Crook;
1094
1095 if ( mpView->GetDragMode() != eMode )
1096 {
1097 mpView->SetDragMode(eMode);
1098 mpView->SetCrookMode(SdrCrookMode::Rotate);
1099 }
1100 }
1101 break;
1102
1103 case SID_OBJECT_CROOK_SLANT:
1104 {
1105 eMode = SdrDragMode::Crook;
1106
1107 if ( mpView->GetDragMode() != eMode )
1108 {
1109 mpView->SetDragMode(eMode);
1110 mpView->SetCrookMode(SdrCrookMode::Slant);
1111 }
1112 }
1113 break;
1114
1115 case SID_OBJECT_CROOK_STRETCH:
1116 {
1117 eMode = SdrDragMode::Crook;
1118
1119 if ( mpView->GetDragMode() != eMode )
1120 {
1121 mpView->SetDragMode(eMode);
1122 mpView->SetCrookMode(SdrCrookMode::Stretch);
1123 }
1124 }
1125 break;
1126
1127 case SID_CONVERT_TO_3D_LATHE:
1128 {
1129 eMode = SdrDragMode::Mirror;
1130 bSuppressChangesOfSelection = true;
1131
1132 if ( mpView->GetDragMode() != eMode )
1133 mpView->SetDragMode(eMode);
1134
1135 if (!mpView->Is3DRotationCreationActive())
1136 mpView->Start3DCreation();
1137
1138 bSuppressChangesOfSelection = false;
1139 }
1140 break;
1141
1142 default:
1143 {
1144 eMode = SdrDragMode::Move;
1145
1146 if ( mpView->GetDragMode() != eMode )
1147 mpView->SetDragMode(eMode);
1148 }
1149 break;
1150 }
1151
1152 if (nSlotId != SID_OBJECT_ROTATE)
1153 {
1154 bTempRotation = false;
1155 }
1156
1157 FuDraw::Activate();
1158 }
1159
SelectionHasChanged()1160 void FuSelection::SelectionHasChanged()
1161 {
1162 bSelectionChanged = true;
1163
1164 FuDraw::SelectionHasChanged();
1165
1166 if (mpView->Is3DRotationCreationActive() && !bSuppressChangesOfSelection)
1167 {
1168 // Switch rotation body -> selection
1169 mpView->ResetCreationActive();
1170 nSlotId = SID_OBJECT_SELECT;
1171 Activate();
1172 }
1173
1174 // Activate the right tool bar for the current context of the view.
1175 mpViewShell->GetViewShellBase().GetToolBarManager()->SelectionHasChanged(*mpViewShell, *mpView);
1176 }
1177
1178 /**
1179 * Set current bezier edit mode
1180 */
SetEditMode(sal_uInt16 nMode)1181 void FuSelection::SetEditMode(sal_uInt16 nMode)
1182 {
1183 nEditMode = nMode;
1184
1185 if (nEditMode == SID_BEZIER_INSERT)
1186 {
1187 mpView->SetInsObjPointMode(true);
1188 }
1189 else
1190 {
1191 mpView->SetInsObjPointMode(false);
1192 }
1193
1194 ForcePointer();
1195
1196 SfxBindings& rBindings = mpViewShell->GetViewFrame()->GetBindings();
1197 rBindings.Invalidate(SID_BEZIER_MOVE);
1198 rBindings.Invalidate(SID_BEZIER_INSERT);
1199 }
1200
1201 /**
1202 * Execute ImageMap interaction
1203 */
HandleImageMapClick(const SdrObject * pObj,const Point & rPos)1204 bool FuSelection::HandleImageMapClick(const SdrObject* pObj, const Point& rPos)
1205 {
1206 bool bClosed = pObj->IsClosedObj();
1207 bool bFilled = false;
1208
1209 if (bClosed)
1210 {
1211 SfxItemSet aSet(mpDoc->GetPool());
1212
1213 aSet.Put(pObj->GetMergedItemSet());
1214
1215 const XFillStyleItem& rFillStyle = aSet.Get(XATTR_FILLSTYLE);
1216 bFilled = rFillStyle.GetValue() != drawing::FillStyle_NONE;
1217 }
1218
1219 const SdrLayerIDSet* pVisiLayer = &mpView->GetSdrPageView()->GetVisibleLayers();
1220 sal_uInt16 nHitLog = sal_uInt16(mpWindow->PixelToLogic(Size(HITPIX, 0)).Width());
1221 const ::tools::Long n2HitLog = nHitLog * 2;
1222 Point aHitPosR(rPos);
1223 Point aHitPosL(rPos);
1224 Point aHitPosT(rPos);
1225 Point aHitPosB(rPos);
1226
1227 aHitPosR.AdjustX(n2HitLog);
1228 aHitPosL.AdjustX(-n2HitLog);
1229 aHitPosT.AdjustY(n2HitLog);
1230 aHitPosB.AdjustY(-n2HitLog);
1231
1232 if (!bClosed || !bFilled
1233 || (SdrObjectPrimitiveHit(*pObj, aHitPosR, nHitLog, *mpView->GetSdrPageView(), pVisiLayer,
1234 false)
1235 && SdrObjectPrimitiveHit(*pObj, aHitPosL, nHitLog, *mpView->GetSdrPageView(),
1236 pVisiLayer, false)
1237 && SdrObjectPrimitiveHit(*pObj, aHitPosT, nHitLog, *mpView->GetSdrPageView(),
1238 pVisiLayer, false)
1239 && SdrObjectPrimitiveHit(*pObj, aHitPosB, nHitLog, *mpView->GetSdrPageView(),
1240 pVisiLayer, false)))
1241 {
1242 if (SvxIMapInfo::GetIMapInfo(pObj))
1243 {
1244 const IMapObject* pIMapObj = SvxIMapInfo::GetHitIMapObject(pObj, rPos);
1245
1246 if (pIMapObj && !pIMapObj->GetURL().isEmpty())
1247 {
1248 // Jump to Document
1249 mpWindow->ReleaseMouse();
1250 SfxStringItem aStrItem(SID_FILE_NAME, pIMapObj->GetURL());
1251 SfxStringItem aReferer(SID_REFERER, mpDocSh->GetMedium()->GetName());
1252 SfxViewFrame* pFrame = mpViewShell->GetViewFrame();
1253 SfxFrameItem aFrameItem(SID_DOCFRAME, pFrame);
1254 SfxBoolItem aBrowseItem(SID_BROWSE, true);
1255 mpWindow->ReleaseMouse();
1256 pFrame->GetDispatcher()->ExecuteList(
1257 SID_OPENDOC, SfxCallMode::ASYNCHRON | SfxCallMode::RECORD,
1258 { &aStrItem, &aFrameItem, &aBrowseItem, &aReferer });
1259
1260 return true;
1261 }
1262 }
1263 }
1264
1265 return false;
1266 }
1267
1268 /** is called when the current function should be aborted. <p>
1269 This is used when a function gets a KEY_ESCAPE but can also
1270 be called directly.
1271
1272 @returns true if an active function was aborted
1273 */
cancel()1274 bool FuSelection::cancel()
1275 {
1276 if (mpView->Is3DRotationCreationActive())
1277 {
1278 mpView->ResetCreationActive();
1279 mpViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_OBJECT_SELECT, SfxCallMode::ASYNCHRON | SfxCallMode::RECORD);
1280 return true;
1281 }
1282 else
1283 {
1284 return false;
1285 }
1286 }
1287
pickObject(const Point & rTestPoint)1288 SdrObject* FuSelection::pickObject (const Point& rTestPoint)
1289 {
1290 SdrPageView* pPageView;
1291 sal_uInt16 nHitLog = sal_uInt16 (mpWindow->PixelToLogic(Size(HITPIX,0)).Width());
1292 return mpView->PickObj(rTestPoint, nHitLog, pPageView, SdrSearchOptions::PICKMARKABLE);
1293 }
1294
ForcePointer(const MouseEvent * pMEvt)1295 void FuSelection::ForcePointer(const MouseEvent* pMEvt)
1296 {
1297 if(bMovedToCenterPoint && !bBeginInsertPoint && pMEvt)
1298 {
1299 MouseEvent aMEvt(pMEvt->GetPosPixel(), pMEvt->GetClicks(),
1300 pMEvt->GetMode(), pMEvt->GetButtons(), pMEvt->GetModifier() & ~KEY_SHIFT);
1301 FuDraw::ForcePointer(&aMEvt);
1302 }
1303 else
1304 {
1305 FuDraw::ForcePointer(pMEvt);
1306 }
1307 }
1308
1309 } // end of namespace sd
1310
1311 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
1312