1 /*
2 Copyright (C) 2011 Elvis Stansvik <elvstone@gmail.com>
3
4 For general Scribus (>=1.3.2) copyright and licensing information please refer
5 to the COPYING file provided with the program. Following this notice may exist
6 a copyright and/or license notice that predates the release of Scribus 1.3.2
7 for which a new license (GPL+exception) is in place.
8 */
9
10 #include "canvasmode_edittable.h"
11
12 #include <QCursor>
13 #include <QDebug>
14 #include <QPainter>
15 #include <QPointF>
16 #include <QTimer>
17
18 #include "appmodes.h"
19 #include "canvas.h"
20 #include "canvasgesture_cellselect.h"
21 #include "canvasgesture_columnresize.h"
22 #include "canvasgesture_rowresize.h"
23 #include "canvasgesture_tableresize.h"
24 #include "cellarea.h"
25 #include "ui/contextmenu.h"
26 #include "fpoint.h"
27 #include "pageitem_table.h"
28 #include "scribus.h"
29 #include "scribusdoc.h"
30 #include "scribusview.h"
31 #include "selection.h"
32 #include "tablehandle.h"
33 #include "ui/scmwmenumanager.h"
34 #include "iconmanager.h"
35
36 // TODO: We should have a preference for this instead.
37 #ifdef Q_OS_MAC
38 static const Qt::KeyboardModifiers CellNavigationModifiers = Qt::AltModifier | Qt::KeypadModifier;
39 #else
40 static const Qt::KeyboardModifiers CellNavigationModifiers = Qt::AltModifier;
41 #endif
42
CanvasMode_EditTable(ScribusView * view)43 CanvasMode_EditTable::CanvasMode_EditTable(ScribusView* view) : CanvasMode(view),
44 m_canvasUpdateTimer(new QTimer(view)),
45 m_selectRowCursor(IconManager::instance().loadCursor("select_row.png")),
46 m_selectColumnCursor(IconManager::instance().loadCursor("select_column.png")),
47 m_tableResizeGesture(new TableResize(this)),
48 m_rowResizeGesture(new RowResize(this)),
49 m_columnResizeGesture(new ColumnResize(this)),
50 m_cellSelectGesture(new CellSelect(this)),
51 m_ScMW(view->m_ScMW)
52 {
53 connect(m_canvasUpdateTimer, SIGNAL(timeout()), this, SLOT(updateCanvas()));
54 }
55
~CanvasMode_EditTable()56 CanvasMode_EditTable::~CanvasMode_EditTable()
57 {
58 delete m_tableResizeGesture;
59 delete m_rowResizeGesture;
60 delete m_columnResizeGesture;
61 delete m_cellSelectGesture;
62 }
63
activate(bool fromGesture)64 void CanvasMode_EditTable::activate(bool fromGesture)
65 {
66 CanvasMode::activate(fromGesture);
67
68 PageItem *item = m_doc->m_Selection->itemAt(0);
69 Q_ASSERT(item && item->isTable());
70 m_table = item->asTable();
71
72 if (fromGesture)
73 m_view->setCursor(Qt::ArrowCursor);
74
75 m_lastCursorPos = -1;
76 m_blinkTime.start();
77 m_canvasUpdateTimer->start(200);
78 makeLongTextCursorBlink();
79
80 m_view->m_ScMW->updateTableMenuActions();
81 }
82
deactivate(bool forGesture)83 void CanvasMode_EditTable::deactivate(bool forGesture)
84 {
85 if (!forGesture)
86 m_canvasUpdateTimer->stop();
87
88 m_view->m_ScMW->updateTableMenuActions();
89 CanvasMode::deactivate(forGesture);
90 }
91
keyPressEvent(QKeyEvent * event)92 void CanvasMode_EditTable::keyPressEvent(QKeyEvent* event)
93 {
94 event->accept();
95
96 // Handle some keys specific to this mode.
97 if (event->key() == Qt::Key_Escape)
98 {
99 // Deselect text in active frame.
100 PageItem_TextFrame* activeFrame = m_table->activeCell().textFrame();
101 activeFrame->itemText.deselectAll();
102 activeFrame->HasSel = false;
103 // Go back to normal mode.
104 m_view->requestMode(modeNormal);
105 }
106
107 // Long text cursor blink on PgUp/PgDown/Up/Down/Home/End.
108 switch (event->key())
109 {
110 case Qt::Key_PageUp:
111 case Qt::Key_PageDown:
112 case Qt::Key_Up:
113 case Qt::Key_Down:
114 case Qt::Key_Home:
115 case Qt::Key_End:
116 makeLongTextCursorBlink();
117 break;
118 }
119
120 // Handle keyboard navigation between cells.
121 if (event->modifiers() == CellNavigationModifiers)
122 {
123 switch (event->key())
124 {
125 case Qt::Key_Left:
126 // Move left
127 m_table->moveLeft();
128 makeLongTextCursorBlink();
129 return;
130 case Qt::Key_Right:
131 // Move right.
132 m_table->moveRight();
133 makeLongTextCursorBlink();
134 return;
135 case Qt::Key_Up:
136 // Move up.
137 m_table->moveUp();
138 makeLongTextCursorBlink();
139 return;
140 case Qt::Key_Down:
141 // Move down.
142 m_table->moveDown();
143 makeLongTextCursorBlink();
144 return;
145 }
146 }
147
148 // Pass all other keys to text frame of active cell.
149 if (!m_table->hasSelection())
150 {
151 bool repeat;
152 m_table->activeCell().textFrame()->handleModeEditKey(event, repeat);
153 }
154 updateCanvas(true);
155 }
156
mouseMoveEvent(QMouseEvent * event)157 void CanvasMode_EditTable::mouseMoveEvent(QMouseEvent* event)
158 {
159 event->accept();
160
161 QPointF canvasPoint = m_canvas->globalToCanvas(event->globalPos()).toQPointF();
162 double threshold = m_doc->guidesPrefs().grabRadius / m_canvas->scale();
163 TableHandle handle = m_table->hitTest(canvasPoint, threshold);
164
165 if (event->buttons() & Qt::LeftButton)
166 {
167 // Mouse is being dragged with left button pressed.
168 handleMouseDrag(event);
169 }
170 else
171 {
172 // Set an appropriate cursor.
173 QCursor cursor(Qt::ArrowCursor);
174 switch (handle.type())
175 {
176 case TableHandle::RowSelect:
177 cursor = m_selectRowCursor;
178 break;
179 case TableHandle::RowResize:
180 cursor = Qt::SizeVerCursor;
181 break;
182 case TableHandle::ColumnSelect:
183 cursor = m_selectColumnCursor;
184 break;
185 case TableHandle::ColumnResize:
186 cursor = Qt::SizeHorCursor;
187 break;
188 case TableHandle::TableResize:
189 cursor = Qt::SizeFDiagCursor;
190 break;
191 case TableHandle::CellSelect:
192 cursor = Qt::IBeamCursor;
193 break;
194 case TableHandle::None:
195 break;
196 default:
197 qWarning("Unknown hit target");
198 break;
199 }
200 m_view->setCursor(cursor);
201 }
202 }
203
mousePressEvent(QMouseEvent * event)204 void CanvasMode_EditTable::mousePressEvent(QMouseEvent* event)
205 {
206 PageItem_TextFrame* activeFrame;
207
208 event->accept();
209 QPointF canvasPoint = m_canvas->globalToCanvas(event->globalPos()).toQPointF();
210 double threshold = m_doc->guidesPrefs().grabRadius / m_canvas->scale();
211 TableHandle handle = m_table->hitTest(canvasPoint, threshold);
212 TableCell cell;
213
214 if (event->button() == Qt::LeftButton)
215 {
216 switch (handle.type())
217 {
218 case TableHandle::RowSelect:
219 // Not implemented.
220 m_table->clearSelection();
221 cell = m_table->cellAt(canvasPoint);
222 if (!cell.isValid())
223 break;
224 // Deselect text in active frame.
225 activeFrame = m_table->activeCell().textFrame();
226 activeFrame->itemText.deselectAll();
227 activeFrame->HasSel = false;
228 // Select row and pre-position text cursor
229 m_table->moveTo(cell);
230 m_table->selectRow(cell.row());
231 m_view->slotSetCurs(event->globalPos().x(), event->globalPos().y());
232 m_lastCursorPos = -1;
233 updateCanvas(true);
234 break;
235 case TableHandle::RowResize:
236 // Start row resize gesture.
237 m_rowResizeGesture->setup(m_table, handle.index());
238 m_view->startGesture(m_rowResizeGesture);
239 break;
240 case TableHandle::ColumnSelect:
241 // Not implemented.
242 m_table->clearSelection();
243 cell = m_table->cellAt(canvasPoint);
244 if (!cell.isValid())
245 break;
246 // Deselect text in active frame.
247 activeFrame = m_table->activeCell().textFrame();
248 activeFrame->itemText.deselectAll();
249 activeFrame->HasSel = false;
250 // Select column and pre-position text cursor
251 m_table->moveTo(cell);
252 m_table->selectColumn(cell.column());
253 m_view->slotSetCurs(event->globalPos().x(), event->globalPos().y());
254 m_lastCursorPos = -1;
255 updateCanvas(true);
256 break;
257 case TableHandle::ColumnResize:
258 // Start column resize gesture.
259 m_columnResizeGesture->setup(m_table, handle.index());
260 m_view->startGesture(m_columnResizeGesture);
261 break;
262 case TableHandle::TableResize:
263 // Start table resize gesture.
264 m_tableResizeGesture->setup(m_table);
265 m_view->startGesture(m_tableResizeGesture);
266 break;
267 case TableHandle::CellSelect:
268 // Move to the pressed cell and position the text cursor.
269 m_table->clearSelection();
270 m_table->moveTo(m_table->cellAt(canvasPoint));
271 m_view->slotSetCurs(event->globalPos().x(), event->globalPos().y());
272 m_lastCursorPos = m_table->activeCell().textFrame()->itemText.cursorPosition();
273 m_view->m_ScMW->setTBvals(m_table->activeCell().textFrame());
274 makeLongTextCursorBlink();
275 updateCanvas(true);
276 break;
277 case TableHandle::None:
278 // Deselect text in active frame.
279 activeFrame = m_table->activeCell().textFrame();
280 activeFrame->itemText.deselectAll();
281 activeFrame->HasSel = false;
282 // Deselect the table and go back to normal mode.
283 m_view->deselectItems(true);
284 m_view->requestMode(modeNormal);
285 m_view->canvasMode()->mousePressEvent(event);
286 break;
287 default:
288 qWarning("Unknown hit target");
289 break;
290 }
291 }
292 else if (event->button() == Qt::RightButton)
293 {
294 // Show the table popup menu.
295 //m_view->m_ScMW->scrMenuMgr->runMenuAtPos("ItemTable", event->globalPos());
296 const FPoint mousePointDoc(m_canvas->globalToCanvas(event->globalPos()));
297 createContextMenu(m_table, mousePointDoc.x(), mousePointDoc.y());
298 }
299 }
300
mouseReleaseEvent(QMouseEvent * event)301 void CanvasMode_EditTable::mouseReleaseEvent(QMouseEvent* event)
302 {
303 m_lastCursorPos = -1;
304 }
305
mouseDoubleClickEvent(QMouseEvent * event)306 void CanvasMode_EditTable::mouseDoubleClickEvent(QMouseEvent* event)
307 {
308 TableCell hitCell = m_table->cellAt(m_canvas->globalToCanvas(event->globalPos()).toQPointF());
309 if (!hitCell.isValid())
310 return;
311
312 event->accept();
313
314 PageItem_TextFrame* textFrame = hitCell.textFrame();
315 if (event->modifiers() & Qt::ControlModifier)
316 {
317 int start = 0, end = 0;
318
319 if (event->modifiers() & Qt::ShiftModifier)
320 {
321 // Double click with Ctrl+Shift to select multiple paragraphs.
322 uint oldParNr = textFrame->itemText.nrOfParagraph(m_lastCursorPos);
323 uint newParNr = textFrame->itemText.nrOfParagraph();
324
325 start = textFrame->itemText.startOfParagraph(qMin(oldParNr, newParNr));
326 end = textFrame->itemText.endOfParagraph(qMax(oldParNr, newParNr));
327 }
328 else
329 {
330 // Double click with Ctrl to select a single paragraph.
331 m_lastCursorPos = textFrame->itemText.cursorPosition();
332 uint parNr = textFrame->itemText.nrOfParagraph(m_lastCursorPos);
333 start = textFrame->itemText.startOfParagraph(parNr);
334 end = textFrame->itemText.endOfParagraph(parNr);
335 }
336 textFrame->itemText.deselectAll();
337 textFrame->itemText.extendSelection(start, end);
338 textFrame->itemText.setCursorPosition(end);
339 }
340 else
341 {
342 // Regular double click selects a word.
343 m_lastCursorPos = textFrame->itemText.cursorPosition();
344 textFrame->itemText.selectWord(textFrame->itemText.cursorPosition());
345 }
346
347 updateCanvas(true);
348 }
349
drawControls(QPainter * p)350 void CanvasMode_EditTable::drawControls(QPainter* p)
351 {
352 p->save();
353 commonDrawControls(p, false);
354 if (!m_table->hasSelection())
355 drawTextCursor(p);
356 p->restore();
357
358 if (m_table->hasSelection())
359 paintCellSelection(p);
360 }
361
paintCellSelection(QPainter * p)362 void CanvasMode_EditTable::paintCellSelection(QPainter* p)
363 {
364 if (!m_table || !m_canvas || !p)
365 return;
366
367 p->save();
368 p->scale(m_canvas->scale(), m_canvas->scale());
369 p->translate(-m_doc->minCanvasCoordinate.x(), -m_doc->minCanvasCoordinate.y());
370 p->setTransform(m_table->getTransform(), true);
371 p->setRenderHint(QPainter::Antialiasing);
372 p->setPen(QPen(QColor(100, 200, 255), 3.0 / m_canvas->scale(), Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
373 p->setBrush(QColor(100, 200, 255, 50));
374
375 /*
376 * The code below makes selectionPath a union of the cell rectangles of the selected cells.
377 * Since the cell rectangles are adjacent, they must be expanded slightly (1.0) for the
378 * uniting to work. This may not be the fastest way to compose the path of the selection,
379 * but it makes for some very simple code. And the result looks good.
380 */
381
382 const QPointF offset = m_table->gridOffset();
383 QPainterPath selectionPath;
384
385 for (const TableCell& cell : m_table->selectedCells())
386 {
387 QRectF cellRect = cell.boundingRect();
388 cellRect.translate(offset);
389 cellRect.adjust(-1.0, -1.0, 1.0, 1.0);
390 QPainterPath cellPath;
391 cellPath.addRect(cellRect);
392 selectionPath = selectionPath.united(cellPath);
393 }
394
395 p->drawPath(selectionPath);
396 p->restore();
397 }
398
updateCanvas(bool forceRedraw)399 void CanvasMode_EditTable::updateCanvas(bool forceRedraw)
400 {
401 // Do not let update timer cancel forced redraw otherwise
402 // we get refresh issues when typing or selecting text
403 if (!m_canvas->isForcedRedraw())
404 m_canvas->setForcedRedraw(forceRedraw);
405 m_canvas->update(m_canvas->canvasToLocal(m_table->getBoundingRect()));
406 }
407
handleMouseDrag(QMouseEvent * event)408 void CanvasMode_EditTable::handleMouseDrag(QMouseEvent* event)
409 {
410 TableCell hitCell = m_table->cellAt(m_canvas->globalToCanvas(event->globalPos()).toQPointF());
411 TableCell activeCell = m_table->activeCell();
412 PageItem_TextFrame* activeFrame = activeCell.textFrame();
413
414 if ((!hitCell.isValid() || hitCell == activeCell) && m_lastCursorPos != -1)
415 {
416 // Select text in active cell text frame.
417 activeFrame->itemText.deselectAll();
418 activeFrame->HasSel = false;
419 m_view->slotSetCurs(event->globalPos().x(), event->globalPos().y());
420
421 PageItem_TextFrame* newActiveFrame = m_table->activeCell().textFrame();
422 if (activeFrame == newActiveFrame)
423 {
424 const int selectionStart = qMin(activeFrame->itemText.cursorPosition(), m_lastCursorPos);
425 const int selectionLength = qAbs(activeFrame->itemText.cursorPosition() - m_lastCursorPos);
426
427 activeFrame->itemText.select(selectionStart, selectionLength);
428 activeFrame->HasSel = (selectionLength > 0);
429 }
430 else
431 {
432 m_lastCursorPos = -1;
433 m_cellSelectGesture->setup(m_table, activeCell);
434 m_view->startGesture(m_cellSelectGesture);
435 }
436 m_view->m_ScMW->setTBvals(newActiveFrame);
437 }
438 else
439 {
440 /*
441 * Mouse moved into another cell, so deselect all text and start
442 * cell selection gesture.
443 */
444 activeFrame->itemText.deselectAll();
445 activeFrame->HasSel = false;
446
447 m_cellSelectGesture->setup(m_table, activeCell);
448 m_view->startGesture(m_cellSelectGesture);
449 }
450
451 updateCanvas(true);
452 }
453
drawTextCursor(QPainter * p)454 void CanvasMode_EditTable::drawTextCursor(QPainter* p)
455 {
456 if ((!m_longBlink && m_blinkTime.elapsed() > qApp->cursorFlashTime() / 2)
457 || (m_longBlink && m_blinkTime.elapsed() > qApp->cursorFlashTime()))
458 {
459 // Reset blink timer
460 m_blinkTime.restart();
461 m_longBlink = false;
462 m_cursorVisible = !m_cursorVisible;
463 }
464
465 if (m_cursorVisible)
466 {
467 // Paint text cursor.
468 p->save();
469 p->setTransform(m_table->getTransform(), true);
470 commonDrawTextCursor(p, m_table->activeCell().textFrame(), m_table->gridOffset());
471 p->restore();
472 }
473 }
474
makeLongTextCursorBlink()475 void CanvasMode_EditTable::makeLongTextCursorBlink()
476 {
477 m_cursorVisible = true;
478 m_longBlink = true;
479 m_blinkTime.restart();
480 }
481
createContextMenu(PageItem * currItem,double mx,double my)482 void CanvasMode_EditTable::createContextMenu(PageItem *currItem, double mx, double my)
483 {
484 ContextMenu* cmen=nullptr;
485 m_view->setCursor(QCursor(Qt::ArrowCursor));
486 m_view->setObjectUndoMode();
487 if (currItem!=nullptr)
488 cmen = new ContextMenu(*(m_doc->m_Selection), m_ScMW, m_doc);
489 else
490 cmen = new ContextMenu(m_ScMW, m_doc, mx, my);
491 if (cmen)
492 cmen->exec(QCursor::pos());
493 m_view->setGlobalUndoMode();
494 delete cmen;
495 }
496