You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdegraphics/kviewshell/documentWidget.cpp

766 lines
22 KiB

//
// Class: DocumentWidget
//
// Widget for displaying TeX DVI files.
// Part of KDVI- A previewer for TeX DVI files.
//
// (C) 2001 Stefan Kebekus
// Copyright (C) 2004-2005 Wilfried Huss <Wilfried.Huss@gmx.at>
// Distributed under the GPL
//
#include <config.h>
#include <tdeaction.h>
#include <tdeapplication.h>
#include <kdebug.h>
#include <tdeglobalsettings.h>
#include <kiconloader.h>
#include <tdelocale.h>
#include <tqclipboard.h>
#include <tqcursor.h>
#include <tqimage.h>
#include <tqpainter.h>
#include <tqpixmap.h>
#include "documentWidget.h"
#include "pageView.h"
#include "documentPageCache.h"
#include "hyperlink.h"
#include "renderedDocumentPagePixmap.h"
#include "textBox.h"
#include "kvsprefs.h"
//#define DEBUG_DOCUMENTWIDGET
const int DocumentWidget::bottom_right_corner[16] =
{ 61, 71, 85, 95,
71, 78, 89, 96,
85, 89, 95, 98,
95, 97, 98, 99 };
const int DocumentWidget::bottom_left_corner[16] =
{ 95, 85, 71, 61,
97, 89, 78, 71,
98, 95, 89, 85,
99, 98, 96, 95 };
const int DocumentWidget::shadow_strip[4] =
{ 56, 67, 83, 94 };
TQColor DocumentWidget::backgroundColorForCorners;
namespace {
/** Holds the icon used as a overlay on pages which are not drawn yet. */
TQPixmap* busyIcon = 0;
/** Internal storages used in the shadow drawing routines in the
drawContents method.*/
TQPixmap* URShadow = 0;
TQPixmap* BRShadow = 0;
TQPixmap* BLShadow = 0;
} // namespace anon
DocumentWidget::DocumentWidget(TQWidget *parent, PageView *sv, DocumentPageCache *cache, const char *name )
: TQWidget( parent, name ), indexOfUnderlinedLink(-1)
{
moveTool = true;
selectionNeedsUpdating = false;
// Variables used in animation.
animationCounter = 0;
timerIdent = 0;
documentCache = cache;
scrollView = sv;
pixmapRequested = false;
scrollGuide = -1;
setMouseTracking(true);
setFocusPolicy(TQWidget::ClickFocus);
connect(&clearStatusBarTimer, TQ_SIGNAL(timeout()), this, TQ_SLOT(clearStatusBar()));
setBackgroundMode(TQt::NoBackground);
if (!busyIcon)
{
busyIcon = new TQPixmap(TDEGlobal::iconLoader()->loadIcon("gear", TDEIcon::NoGroup, TDEIcon::SizeMedium));
URShadow = new TQPixmap();
BRShadow = new TQPixmap();
BLShadow = new TQPixmap();
URShadow->resize(4,4);
BRShadow->resize(4,4);
BLShadow->resize(4,4);
}
}
void DocumentWidget::setPageNumber(TQ_UINT16 nr)
{
pageNr = nr;
selectionNeedsUpdating = true;
// We have to reset this, because otherwise we might crash in the mouseMoveEvent
// After switching pages in SinglePageMode or OverviewMode.
indexOfUnderlinedLink = -1;
// Resize Widget if the size of the new page is different than the size of the old page.
TQSize _pageSize = documentCache->sizeOfPageInPixel(pageNr);
if (_pageSize != pageSize())
{
setPageSize(_pageSize);
}
update();
}
TQRect DocumentWidget::linkFlashRect()
{
int width = pageSize().width()/(11 - animationCounter);
int height = pageSize().height()/(60 - 4 * animationCounter);
return TQRect((pageSize().width()-width)/2, flashOffset - height/2, width, height);
}
void DocumentWidget::timerEvent( TQTimerEvent *e )
{
if (animationCounter == 0) {
killTimer(e->timerId());
timerIdent = startTimer(50); // Proceed with the animation in 1/10s intervals
}
animationCounter++;
TQRect flashRect = linkFlashRect();
flashRect.addCoords(-1, -1, 1, 1);
if (animationCounter >= 10) {
killTimer(e->timerId());
timerIdent = 0;
animationCounter = 0;
}
repaint(flashRect, false);
}
void DocumentWidget::flash(int fo)
{
if (timerIdent != 0)
{
killTimer(timerIdent);
// Delete old flash rectangle
animationCounter = 10;
TQRect flashRect = linkFlashRect();
flashRect.addCoords(-1, -1, 1, 1);
repaint(flashRect, false);
}
animationCounter = 0;
flashOffset = fo;
timerIdent = startTimer(50); // Start the animation. The animation proceeds in 1/10s intervals
}
void DocumentWidget::paintEvent(TQPaintEvent *e)
{
#ifdef DEBUG_DOCUMENTWIDGET
kdDebug(1223) << "DocumentWidget::paintEvent() called" << endl;
#endif
// Check if this widget is really visible to the user. If not, there
// is nothing to do. Remark: if we don't do this, then under QT
// 3.2.3 the following happens: when the user changes the zoom
// value, all those widgets are updated which the user has EVER
// seen, not just those that are visible at the moment. If the
// document contains several thousand pages, it is easily possible
// that this means that a few hundred of these are re-painted (which
// takes substantial time) although perhaps only three widgets are
// visible and *should* be updated. I believe this is some error in
// QT, but I am not positive about that ---Stefan Kebekus.
if (!isVisible())
{
//kdDebug() << "widget of page " << pageNr << " is not visible. Abort rendering" << endl;
kapp->processEvents();
return;
}
TQPainter p(this);
p.setClipRegion(e->region());
// Paint a black border around the widget
p.setRasterOp(TQt::CopyROP);
p.setBrush(NoBrush);
p.setPen(TQt::black);
TQRect outlineRect = pageRect();
outlineRect.addCoords(-1, -1, 1, 1);
p.drawRect(outlineRect);
// Paint page shadow
TQColor backgroundColor = colorGroup().mid();
// (Re-)generate the Pixmaps for the shadow corners, if necessary
if (backgroundColor != backgroundColorForCorners)
{
backgroundColorForCorners = backgroundColor;
TQImage tmp(4, 4, 32);
for(int x=0; x<4; x++)
for(int y=0; y<4; y++)
tmp.setPixel(x, y, backgroundColor.light(bottom_right_corner[x+4*y]).rgb() );
BRShadow->convertFromImage(tmp);
for(int x=0; x<4; x++)
for(int y=0; y<4; y++)
tmp.setPixel(x, y, backgroundColor.light(bottom_left_corner[x+4*y]).rgb() );
BLShadow->convertFromImage(tmp);
URShadow->convertFromImage(tmp.mirror(true, true));
}
// Draw right and bottom shadows
for(int i=0; i<4; i++)
{
p.setPen(backgroundColor.light(shadow_strip[i]));
// Right shadow
p.drawLine(pageSize().width()+i+2, 8, pageSize().width()+i+2, pageSize().height()+2);
// Bottom shadow
p.drawLine(8, pageSize().height()+i+2, pageSize().width()+2, pageSize().height()+i+2);
}
// Draw shadow corners
p.drawPixmap(pageSize().width()+2, pageSize().height()+2, *BRShadow);
p.drawPixmap(4, pageSize().height()+2, *BLShadow);
p.drawPixmap(pageSize().width()+2, 4, *URShadow);
// Draw corners
p.fillRect(0, pageSize().height()+2, 4, 4, backgroundColor);
p.fillRect(pageSize().width()+2, 0, 4, 4, backgroundColor);
if (!documentCache->isPageCached(pageNr, pageSize()))
{
TQRect destRect = e->rect().intersect(pageRect());
if (KVSPrefs::changeColors() && KVSPrefs::renderMode() == KVSPrefs::EnumRenderMode::Paper)
p.fillRect(destRect, KVSPrefs::paperColor());
else
p.fillRect(destRect, TQt::white);
// Draw busy indicator.
// Im not really sure if this is a good idea.
// While it is nice to see an indication that something is happening for pages which
// take long to redraw, it gets quite annoing for fast redraws.
// TODO: Disable or find something less distractiong.
p.drawPixmap(10, 10, *busyIcon);
if (!pixmapRequested)
{
// Request page pixmap.
pixmapRequested = true;
TQTimer::singleShot(50, this, TQ_SLOT(delayedRequestPage()));
}
return;
}
RenderedDocumentPagePixmap *pageData = documentCache->getPage(pageNr);
if (pageData == 0) {
#ifdef DEBUG_DOCUMENTWIDGET
kdDebug(1223) << "DocumentWidget::paintEvent: no documentPage generated" << endl;
#endif
return;
}
TQMemArray<TQRect> damagedRects = TQRegion(e->region()).rects();
for (unsigned int i = 0; i < damagedRects.count(); i++)
{
// Paint the page where it intersects with the damaged area.
TQRect destRect = damagedRects[i].intersect(pageRect());
// The actual page starts at point (1,1) because of the outline.
// Therefore we need to shift the destination rectangle.
TQRect pixmapRect = destRect;
pixmapRect.moveBy(-1,-1);
if (KVSPrefs::changeColors() && KVSPrefs::renderMode() != KVSPrefs::EnumRenderMode::Paper)
{
// Paint widget contents with accessibility changes.
TQPixmap pdap = pageData->accessiblePixmap();
bitBlt ( this, destRect.topLeft(), &pdap, pixmapRect, CopyROP);
}
else
{
// Paint widget contents
bitBlt ( this, destRect.topLeft(), pageData, pixmapRect, CopyROP);
}
}
// Underline hyperlinks
if (KVSPrefs::underlineLinks() == KVSPrefs::EnumUnderlineLinks::Enabled ||
KVSPrefs::underlineLinks() == KVSPrefs::EnumUnderlineLinks::OnlyOnHover)
{
int h = 2; // Height of line.
for(int i = 0; i < (int)pageData->hyperLinkList.size(); i++)
{
if (KVSPrefs::underlineLinks() == KVSPrefs::EnumUnderlineLinks::OnlyOnHover &&
i != indexOfUnderlinedLink)
continue;
int x = pageData->hyperLinkList[i].box.left();
int w = pageData->hyperLinkList[i].box.width();
int y = pageData->hyperLinkList[i].baseline;
TQRect hyperLinkRect(x, y, w, h);
if (hyperLinkRect.intersects(e->rect()))
{
#ifdef DEBUG_DOCUMENTWIDGET
kdDebug(1223) << "Underline hyperlink \"" << pageData->hyperLinkList[i].linkText << "\"" << endl;
#endif
p.fillRect(x, y, w, h, TDEGlobalSettings::linkColor());
}
}
}
// Paint flashing frame, if appropriate
if (animationCounter > 0 && animationCounter < 10)
{
int gbChannel = 255 - (9-animationCounter)*28;
p.setPen(TQPen(TQColor(255, gbChannel, gbChannel), 3));
p.drawRect(linkFlashRect());
}
// Mark selected text.
TextSelection selection = documentCache->selectedText();
if ((selection.getPageNumber() != 0) && (selection.getPageNumber() == pageNr))
{
if (selectionNeedsUpdating)
{
//The zoom value has changed, therefore we need to recalculate
//the selected region.
selectedRegion = pageData->selectedRegion(selection);
selectionNeedsUpdating = false;
}
p.setPen(NoPen);
p.setBrush(white);
p.setRasterOp(TQt::XorROP);
TQMemArray<TQRect> selectionRects = selectedRegion.rects();
for (unsigned int i = 0; i < selectionRects.count(); i++)
p.drawRect(selectionRects[i]);
}
// Draw scroll Guide
if (scrollGuide >= 0)
{
// Don't draw over the page shadow
p.setClipRegion(e->region().intersect(pageRect()));
p.setRasterOp(TQt::CopyROP);
p.setPen(TQt::red);
p.drawLine(1, scrollGuide, pageSize().width(), scrollGuide);
}
}
void DocumentWidget::drawScrollGuide(int ycoord)
{
//kdDebug() << "draw scroll guide for page " << pageNr << " at y = " << ycoord << endl;
scrollGuide = ycoord;
update(TQRect(1, scrollGuide, pageSize().width(), 1));
TQTimer::singleShot(1000, this, TQ_SLOT(clearScrollGuide()));
}
void DocumentWidget::clearScrollGuide()
{
//kdDebug() << "clear scroll guide for page " << pageNr << " at y = " << scrollGuide << endl;
int temp = scrollGuide;
scrollGuide = -1;
update(TQRect(1, temp, pageSize().width(), 1));
}
void DocumentWidget::select(const TextSelection& newSelection)
{
// Get a pointer to the page contents
RenderedDocumentPage *pageData = documentCache->getPage(pageNr);
if (pageData == 0) {
kdDebug(1223) << "DocumentWidget::select() pageData for page #" << pageNr << " is empty" << endl;
return;
}
documentCache->selectText(newSelection);
selectedRegion = pageData->selectedRegion(documentCache->selectedText());
selectionNeedsUpdating = false;
update();
}
void DocumentWidget::selectAll()
{
// pageNr == 0 indicated an invalid page (e.g. page number not yet
// set)
if (pageNr == 0)
return;
// Get a pointer to the page contents
RenderedDocumentPage *pageData = documentCache->getPage(pageNr);
if (pageData == 0) {
kdDebug(1223) << "DocumentWidget::selectAll() pageData for page #" << pageNr << " is empty" << endl;
return;
}
TextSelection selection;
// mark everything as selected
TQString selectedText("");
for(unsigned int i = 0; i < pageData->textBoxList.size(); i++) {
selectedText += pageData->textBoxList[i].text;
selectedText += "\n";
}
selection.set(pageNr, 0, pageData->textBoxList.size()-1, selectedText);
selectedRegion = pageData->selectedRegion(selection);
documentCache->selectText(selection);
// Re-paint
update();
}
void DocumentWidget::mousePressEvent ( TQMouseEvent * e )
{
#ifdef DEBUG_DOCUMENTWIDGET
kdDebug(1223) << "DocumentWidget::mousePressEvent(...) called" << endl;
#endif
// Make sure the event is passed on to the higher-level widget;
// otherwise QT gets the coordinated in the mouse move events wrong
e->ignore();
// pageNr == 0 indicated an invalid page (e.g. page number not yet
// set)
if (pageNr == 0)
return;
// Get a pointer to the page contents
RenderedDocumentPage *pageData = documentCache->getPage(pageNr);
if (pageData == 0) {
kdDebug(1223) << "DocumentWidget::selectAll() pageData for page #" << pageNr << " is empty" << endl;
return;
}
// Check if the mouse is pressed on a regular hyperlink
if (e->button() == TQt::LeftButton) {
if (pageData->hyperLinkList.size() > 0)
for(unsigned int i = 0; i < pageData->hyperLinkList.size(); i++) {
if (pageData->hyperLinkList[i].box.contains(e->pos())) {
emit(localLink(pageData->hyperLinkList[i].linkText));
return;
}
}
if (moveTool)
setCursor(TQt::SizeAllCursor);
else
setCursor(TQt::IbeamCursor);
}
if (e->button() == TQt::RightButton || (!moveTool && e->button() == TQt::LeftButton))
{
setCursor(TQt::IbeamCursor);
// If Shift is not pressed clear the current selection,
// otherwise modify the existing selection.
if (!(e->state() & ShiftButton))
{
firstSelectedPoint = e->pos();
selectedRectangle = TQRect();
selectedRegion = TQRegion();
emit clearSelection();
}
}
}
void DocumentWidget::mouseReleaseEvent ( TQMouseEvent *e )
{
// Make sure the event is passed on to the higher-level widget;
// otherwise the mouse cursor in the centeringScrollview is wrong
e->ignore();
if (e->button() == TQt::RightButton || (!moveTool && e->button() == TQt::LeftButton))
{
// If the selectedRectangle is empty then there was only a single right click.
if (firstSelectedPoint == e->pos())
{
if (pageNr == 0)
return;
// Get a pointer to the page contents
RenderedDocumentPage* pageData = documentCache->getPage(pageNr);
if (pageData == 0)
{
kdDebug(1223) << "DocumentWidget::mouseReleaseEvent() pageData for page #" << pageNr << " is empty" << endl;
return;
}
TextSelection newTextSelection = pageData->select(firstSelectedPoint);
updateSelection(newTextSelection);
}
}
//Reset the cursor to the usual arrow if we use the move tool, and
// The textselection cursor if we use the selection tool.
setStandardCursor();
}
void DocumentWidget::mouseMoveEvent ( TQMouseEvent * e )
{
#ifdef DEBUG_DOCUMENTWIDGET
kdDebug(1223) << "DocumentWidget::mouseMoveEvent(...) called" << endl;
#endif
// pageNr == 0 indicated an invalid page (e.g. page number not yet
// set)
if (pageNr == 0)
return;
// Get a pointer to the page contents
RenderedDocumentPage *pageData = documentCache->getPage(pageNr);
if (pageData == 0) {
kdDebug(1223) << "DocumentWidget::selectAll() pageData for page #" << pageNr << " is empty" << endl;
return;
}
// If no mouse button pressed
if (e->state() == 0) {
// Remember the index of the underlined link.
int lastUnderlinedLink = indexOfUnderlinedLink;
// go through hyperlinks
for(unsigned int i = 0; i < pageData->hyperLinkList.size(); i++) {
if (pageData->hyperLinkList[i].box.contains(e->pos())) {
clearStatusBarTimer.stop();
setCursor(pointingHandCursor);
TQString link = pageData->hyperLinkList[i].linkText;
if ( link.startsWith("#") )
link = link.remove(0,1);
emit setStatusBarText( i18n("Link to %1").arg(link) );
indexOfUnderlinedLink = i;
if (KVSPrefs::underlineLinks() == KVSPrefs::EnumUnderlineLinks::OnlyOnHover &&
indexOfUnderlinedLink != lastUnderlinedLink)
{
TQRect newUnderline = pageData->hyperLinkList[i].box;
// Increase Rectangle so that the whole line really lines in it.
newUnderline.addCoords(0, 0, 0, 2);
// Redraw widget
update(newUnderline);
if (lastUnderlinedLink != -1 && lastUnderlinedLink < pageData->hyperLinkList.size())
{
// Erase old underline
TQRect oldUnderline = pageData->hyperLinkList[lastUnderlinedLink].box;
oldUnderline.addCoords(0, 0, 0, 2);
update(oldUnderline);
}
}
return;
}
}
// Whenever we reach this the mouse hovers no link.
indexOfUnderlinedLink = -1;
if (KVSPrefs::underlineLinks() == KVSPrefs::EnumUnderlineLinks::OnlyOnHover &&
lastUnderlinedLink != -1 && lastUnderlinedLink < pageData->hyperLinkList.size())
{
// Erase old underline
TQRect oldUnderline = pageData->hyperLinkList[lastUnderlinedLink].box;
// Increase Rectangle so that the whole line really lines in it.
oldUnderline.addCoords(0, 0, 0, 2);
// Redraw widget
update(oldUnderline);
}
// Cursor not over hyperlink? Then let the cursor be the usual arrow if we use the move tool, and
// The textselection cursor if we use the selection tool.
setStandardCursor();
}
if (!clearStatusBarTimer.isActive())
clearStatusBarTimer.start(200, true); // clear the statusbar after 200 msec.
// Left mouse button pressed -> Text scroll function
if ((e->state() & TQt::LeftButton) != 0 && moveTool)
{
// Pass the mouse event on to the owner of this widget ---under
// normal circumstances that is the centeringScrollView which will
// then scroll the scrollview contents
e->ignore();
}
// Right mouse button pressed -> Text copy function
if ((e->state() & TQt::RightButton) != 0 || (!moveTool && (e->state() & TQt::LeftButton != 0)))
{
if (selectedRectangle.isEmpty()) {
firstSelectedPoint = e->pos();
selectedRectangle.setRect(e->pos().x(),e->pos().y(),1,1);
} else {
int lx = e->pos().x() < firstSelectedPoint.x() ? e->pos().x() : firstSelectedPoint.x();
int rx = e->pos().x() > firstSelectedPoint.x() ? e->pos().x() : firstSelectedPoint.x();
int ty = e->pos().y() < firstSelectedPoint.y() ? e->pos().y() : firstSelectedPoint.y();
int by = e->pos().y() > firstSelectedPoint.y() ? e->pos().y() : firstSelectedPoint.y();
selectedRectangle.setCoords(lx,ty,rx,by);
}
// Now that we know the rectangle, we have to find out which words
// intersect it!
TextSelection newTextSelection = pageData->select(selectedRectangle);
updateSelection(newTextSelection);
}
}
void DocumentWidget::updateSelection(const TextSelection& newTextSelection)
{
if (newTextSelection != documentCache->selectedText())
{
if (newTextSelection.isEmpty())
{
// Clear selection
documentCache->deselectText();
selectedRectangle = TQRect();
selectedRegion = TQRegion();
update();
}
else
{
if (pageNr == 0)
return;
// Get a pointer to the page contents
RenderedDocumentPage* pageData = documentCache->getPage(pageNr);
if (pageData == 0)
{
kdDebug(1223) << "DocumentWidget::mouseReleaseEvent() pageData for page #" << pageNr << " is empty" << endl;
return;
}
documentCache->selectText(newTextSelection);
TQRegion newlySelectedRegion = pageData->selectedRegion(documentCache->selectedText());
// Compute the region that needs to be updated
TQRegion updateRegion;
if(!selectedRegion.isEmpty())
{
updateRegion = newlySelectedRegion.eor(selectedRegion);
}
else
{
updateRegion = newlySelectedRegion;
}
selectedRegion = newlySelectedRegion;
TQMemArray<TQRect> rectangles = updateRegion.rects();
for (unsigned int i = 0; i < rectangles.count(); i++)
{
repaint(rectangles[i]);
}
}
}
}
void DocumentWidget::clearStatusBar()
{
emit setStatusBarText( TQString() );
}
void DocumentWidget::delayedRequestPage()
{
if (!isVisible())
{
// We only want to calculate the page pixmap for widgets that are currently visible.
// When we are fast scrolling thru the document many paint events are created, that
// are often not needed anymore at the time the eventloop executes them.
//kdDebug() << "delayedRequest: widget of page " << pageNr << " is not visible. Abort rendering" << endl;
pixmapRequested = false;
kapp->processEvents();
return;
}
documentCache->getPage(pageNr);
pixmapRequested = false;
update();
// If more widgets need updateing at the some time, the next line allows them to be
// displayed one after another. Widthout it all widgets are updated after all the rendering
// is completed. This is especially noticable in overview mode. After the change to a seperate
// rendering thread this will probably not be needed anymore.
kapp->processEvents();
}
TQSize DocumentWidget::pageSize() const
{
// Substract size of the shadow.
return size() - TQSize(6, 6);
}
TQRect DocumentWidget::pageRect() const
{
TQRect boundingRect = rect();
// Substract the shadow.
boundingRect.addCoords(1,1,-5,-5);
return boundingRect;
}
void DocumentWidget::setPageSize(const TQSize& pageSize)
{
// When the page size changes this means either the paper size
// has been changed, or the zoomlevel has been changed.
// If the zoomlevel changes we need to recalculate the selected
// region. We do this always, just to be on the safe side.
selectionNeedsUpdating = true;
// Add size of the shadow.
resize(pageSize + TQSize(6,6));
}
void DocumentWidget::setPageSize(int width, int height)
{
setPageSize(TQSize(width, height));
}
void DocumentWidget::slotEnableMoveTool(bool enable)
{
moveTool = enable;
setStandardCursor();
}
void DocumentWidget::setStandardCursor()
{
if (moveTool)
{
setCursor(TQt::arrowCursor);
}
else
{
setCursor(TQt::IbeamCursor);
}
}
bool DocumentWidget::isVisible()
{
TQRect visibleRect(scrollView->contentsX(), scrollView->contentsY(), scrollView->visibleWidth(), scrollView->visibleHeight());
TQRect widgetRect(scrollView->childX(this), scrollView->childY(this), width(), height());
return widgetRect.intersects(visibleRect);
}
#include "documentWidget.moc"