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