/***************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2007 Rivo Laks You can Freely distribute this program under the GNU General Public License. See the file "COPYING" for the exact licensing terms. ******************************************************************/ #include "presentwindows.h" #include #include #include #include #include #include #include #include namespace KWin { KWIN_EFFECT( PresentWindows, PresentWindowsEffect ) PresentWindowsEffect::PresentWindowsEffect() : mShowWindowsFromAllDesktops ( false ) , mActivated( false ) , mActiveness( 0.0 ) , mRearranging( 1.0 ) , hasKeyboardGrab( false ) , mHoverWindow( NULL ) #ifdef HAVE_OPENGL , filterTexture( NULL ) #endif { KActionCollection* actionCollection = new KActionCollection( this ); KAction* a = (KAction*)actionCollection->addAction( "Expose" ); a->setText( i18n("Toggle Expose effect" )); a->setGlobalShortcut(KShortcut(Qt::CTRL + Qt::Key_F10)); connect(a, SIGNAL(triggered(bool)), this, SLOT(toggleActive())); KAction* b = (KAction*)actionCollection->addAction( "ExposeAll" ); b->setText( i18n("Toggle Expose effect (incl other desktops)" )); b->setGlobalShortcut(KShortcut(Qt::CTRL + Qt::Key_F11)); connect(b, SIGNAL(triggered(bool)), this, SLOT(toggleActiveAllDesktops())); borderActivate = ElectricTopRight; // TODO config options borderActivateAll = ElectricNone; effects->reserveElectricBorder( borderActivate ); effects->reserveElectricBorder( borderActivateAll ); } PresentWindowsEffect::~PresentWindowsEffect() { effects->unreserveElectricBorder( borderActivate ); effects->unreserveElectricBorder( borderActivateAll ); discardFilterTexture(); } void PresentWindowsEffect::prePaintScreen( int* mask, QRegion* region, int time ) { // How long does it take for the effect to get it's full strength (in ms) const float changeTime = 300; if(mActivated) { mActiveness = qMin(1.0f, mActiveness + time/changeTime); if( mRearranging < 1 ) mRearranging = qMin(1.0f, mRearranging + time/changeTime); } else if(mActiveness > 0.0f) { mActiveness = qMax(0.0f, mActiveness - time/changeTime); if(mActiveness <= 0.0f) effectTerminated(); } // We need to mark the screen windows as transformed. Otherwise the whole // screen won't be repainted, resulting in artefacts if( mActiveness > 0.0f ) *mask |= Effect::PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; effects->prePaintScreen(mask, region, time); } void PresentWindowsEffect::prePaintWindow( EffectWindow* w, int* mask, QRegion* paint, QRegion* clip, int time ) { if( mActiveness > 0.0f ) { if( mWindowData.contains(w) ) { // This window will be transformed by the effect *mask |= Effect::PAINT_WINDOW_TRANSFORMED; w->enablePainting( EffectWindow::PAINT_DISABLED_BY_MINIMIZE ); w->enablePainting( EffectWindow::PAINT_DISABLED_BY_DESKTOP ); // If it's minimized window or on another desktop and effect is not // fully active, then apply some transparency if( mActiveness < 1.0f && (w->isMinimized() || !w->isOnCurrentDesktop() )) *mask |= Effect::PAINT_WINDOW_TRANSLUCENT; // Change window's hover according to cursor pos WindowData& windata = mWindowData[w]; const float hoverchangetime = 200; if( windata.area.contains(cursorPos()) ) windata.hover = qMin(1.0f, windata.hover + time / hoverchangetime); else windata.hover = qMax(0.0f, windata.hover - time / hoverchangetime); } else if( !w->isDesktop()) w->disablePainting( EffectWindow::PAINT_DISABLED ); } effects->prePaintWindow( w, mask, paint, clip, time ); } void PresentWindowsEffect::paintScreen( int mask, QRegion region, ScreenPaintData& data ) { effects->paintScreen( mask, region, data ); #ifdef HAVE_OPENGL if( filterTexture && region.intersects( filterTextureRect )) { glPushAttrib( GL_CURRENT_BIT | GL_ENABLE_BIT ); filterTexture->bind(); glEnable( GL_BLEND ); glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA ); glEnableClientState( GL_VERTEX_ARRAY ); int x = filterTextureRect.x(); int y = filterTextureRect.y(); int width = filterTextureRect.width(); int height = filterTextureRect.height(); int verts[ 4 * 2 ] = { x, y, x, y + height, x + width, y + height, x + width, y }; glVertexPointer( 2, GL_INT, 0, verts ); glEnableClientState( GL_TEXTURE_COORD_ARRAY ); int texcoords[ 4 * 2 ] = { 0, 1, 0, 0, 1, 0, 1, 1 }; glTexCoordPointer( 2, GL_INT, 0, texcoords ); glDrawArrays( GL_QUADS, 0, 4 ); glDisableClientState( GL_TEXTURE_COORD_ARRAY ); glDisableClientState( GL_VERTEX_ARRAY ); filterTexture->unbind(); glPopAttrib(); } #endif } void PresentWindowsEffect::paintWindow( EffectWindow* w, int mask, QRegion region, WindowPaintData& data ) { if(mActiveness > 0.0f && mWindowData.contains(w)) { // Change window's position and scale const WindowData& windata = mWindowData[w]; if( mRearranging < 1 ) // rearranging { if( windata.old_area.isEmpty()) // no old position { data.xScale = windata.scale; data.yScale = windata.scale; data.xTranslate = windata.area.left() - w->x(); data.yTranslate = windata.area.top() - w->y(); data.opacity *= interpolate(0.0, 1.0, mRearranging); } else { data.xScale = interpolate(windata.old_scale, windata.scale, mRearranging); data.yScale = interpolate(windata.old_scale, windata.scale, mRearranging); data.xTranslate = (int)interpolate(windata.old_area.left() - w->x(), windata.area.left() - w->x(), mRearranging); data.yTranslate = (int)interpolate(windata.old_area.top() - w->y(), windata.area.top() - w->y(), mRearranging); } } else { data.xScale = interpolate(data.xScale, windata.scale, mActiveness); data.yScale = interpolate(data.xScale, windata.scale, mActiveness); data.xTranslate = (int)interpolate(data.xTranslate, windata.area.left() - w->x(), mActiveness); data.yTranslate = (int)interpolate(data.yTranslate, windata.area.top() - w->y(), mActiveness); } // Darken all windows except for the one under the cursor data.brightness *= interpolate(1.0, 0.7, mActiveness * (1.0f - windata.hover)); // If it's minimized window or on another desktop and effect is not // fully active, then apply some transparency if( mActiveness < 1.0f && (w->isMinimized() || !w->isOnCurrentDesktop() )) data.opacity *= interpolate(0.0, 1.0, mActiveness); } // Call the next effect. effects->paintWindow( w, mask, region, data ); } void PresentWindowsEffect::postPaintScreen() { if( mActivated && mActiveness < 1.0 ) // activating effect effects->addRepaintFull(); if( mActivated && mRearranging < 1.0 ) // rearranging effects->addRepaintFull(); if( !mActivated && mActiveness > 0.0 ) // deactivating effect effects->addRepaintFull(); foreach( const WindowData& d, mWindowData ) { if( d.hover > 0 && d.hover < 1 ) // changing highlight effects->addRepaintFull(); } // Call the next effect. effects->postPaintScreen(); } void PresentWindowsEffect::windowInputMouseEvent( Window w, QEvent* e ) { assert( w == mInput ); if( e->type() == QEvent::MouseMove ) { // Repaint if the hovered-over window changed. // (No need to use cursorMoved(), this takes care of it as well) for( DataHash::ConstIterator it = mWindowData.begin(); it != mWindowData.end(); ++it ) { if( (*it).area.contains( cursorPos())) { if( mHoverWindow != it.key()) { mHoverWindow = it.key(); effects->addRepaintFull(); // screen is transformed, so paint all } return; } } return; } if( e->type() != QEvent::MouseButtonPress ) return; if( static_cast< QMouseEvent* >( e )->button() != Qt::LeftButton ) { setActive( false ); return; } // Find out which window (if any) was clicked and activate it QPoint pos = static_cast< QMouseEvent* >( e )->pos(); for( DataHash::iterator it = mWindowData.begin(); it != mWindowData.end(); ++it ) { if( it.value().area.contains(pos) ) { effects->activateWindow( it.key() ); // mWindowData gets cleared and rebuilt when a window is // activated, so it's dangerous (and unnecessary) to continue break; } } // Deactivate effect, no matter if any window was actually activated setActive(false); } void PresentWindowsEffect::windowClosed( EffectWindow* w ) { if( mHoverWindow == w ) mHoverWindow = NULL; mWindowsToPresent.remove( w ); rearrangeWindows(); } void PresentWindowsEffect::setActive(bool active) { if( mActivated == active ) return; mActivated = active; mHoverWindow = NULL; if( mActivated ) { mWindowData.clear(); effectActivated(); mActiveness = 0; windowFilter.clear(); mWindowsToPresent.clear(); const EffectWindowList& originalwindowlist = effects->stackingOrder(); // Filter out special windows such as panels and taskbars foreach( EffectWindow* window, originalwindowlist ) { if( window->isSpecialWindow() ) continue; if( window->isDeleted()) continue; if( !mShowWindowsFromAllDesktops && !window->isOnCurrentDesktop() ) continue; mWindowsToPresent.append(window); } rearrangeWindows(); } else { mWindowsToPresent.clear(); mRearranging = 1; // turn off mActiveness = 1; // go back from arranged position discardFilterTexture(); } effects->addRepaintFull(); // trigger next animation repaint } void PresentWindowsEffect::effectActivated() { // Create temporary input window to catch mouse events mInput = effects->createFullScreenInputWindow( this, Qt::PointingHandCursor ); hasKeyboardGrab = effects->grabKeyboard( this ); } void PresentWindowsEffect::effectTerminated() { // Destroy the temporary input window effects->destroyInputWindow( mInput ); if( hasKeyboardGrab ) effects->ungrabKeyboard(); hasKeyboardGrab = false; } void PresentWindowsEffect::rearrangeWindows() { if( !mActivated ) return; EffectWindowList windowlist; if( windowFilter.isEmpty()) windowlist = mWindowsToPresent; else { foreach( EffectWindow* w, mWindowsToPresent ) { if( w->caption().contains( windowFilter, Qt::CaseInsensitive ) || w->windowClass().contains( windowFilter, Qt::CaseInsensitive )) windowlist.append( w ); } } if( windowlist.isEmpty()) { mWindowData.clear(); effects->addRepaintFull(); return; } if( !mWindowData.isEmpty()) // this is not the first arranging { bool rearrange = canRearrangeClosest( windowlist ); // called before manipulating mWindowData DataHash newdata; EffectWindowList newlist = windowlist; EffectWindowList oldlist = mWindowData.keys(); qSort( newlist ); qSort( oldlist ); for( DataHash::ConstIterator it = mWindowData.begin(); it != mWindowData.end(); ++it ) if( windowlist.contains( it.key())) // remove windows that are not in the window list newdata[ it.key() ] = *it; mWindowData = newdata; if( !rearrange && newlist == oldlist ) return; for( DataHash::Iterator it = mWindowData.begin(); it != mWindowData.end(); ++it ) { (*it).old_area = (*it).area; (*it).old_scale = (*it).scale; } mRearranging = 0; // start animation again } // Calculate new positions and scales for windows // calculateWindowTransformationsDumb( windowlist ); // calculateWindowTransformationsKompose( windowlist ); calculateWindowTransformationsClosest( windowlist ); // Schedule entire desktop to be repainted effects->addRepaintFull(); } void PresentWindowsEffect::calculateWindowTransformationsDumb(EffectWindowList windowlist) { // Calculate number of rows/cols int rows = windowlist.count() / 4 + 1; int cols = windowlist.count() / rows + windowlist.count() % rows; // Get rect which we can use on current desktop. This excludes e.g. panels QRect placementRect = effects->clientArea( PlacementArea, QPoint( 0, 0 ), 0 ); // Size of one cell int cellwidth = placementRect.width() / cols; int cellheight = placementRect.height() / rows; kDebug() << k_funcinfo << "Got " << windowlist.count() << " clients, using " << rows << "x" << cols << " grid" << endl; // Calculate position and scale factor for each window int i = 0; foreach( EffectWindow* window, windowlist ) { // Row/Col of this window int r = i / cols; int c = i % cols; mWindowData[window].hover = 0.0f; mWindowData[window].scale = qMin(cellwidth / (float)window->width(), cellheight / (float)window->height()); mWindowData[window].area.setLeft(placementRect.left() + cellwidth * c); mWindowData[window].area.setTop(placementRect.top() + cellheight * r); mWindowData[window].area.setWidth((int)(window->width() * mWindowData[window].scale)); mWindowData[window].area.setHeight((int)(window->height() * mWindowData[window].scale)); kDebug() << k_funcinfo << "Window '" << window->caption() << "' gets moved to (" << mWindowData[window].area.left() << "; " << mWindowData[window].area.right() << "), scale: " << mWindowData[window].scale << endl; i++; } } float PresentWindowsEffect::windowAspectRatio(EffectWindow* c) { return c->width() / (float)c->height(); } int PresentWindowsEffect::windowWidthForHeight(EffectWindow* c, int h) { return (int)((h / (float)c->height()) * c->width()); } int PresentWindowsEffect::windowHeightForWidth(EffectWindow* c, int w) { return (int)((w / (float)c->width()) * c->height()); } void PresentWindowsEffect::calculateWindowTransformationsKompose(EffectWindowList windowlist) { // Get rect which we can use on current desktop. This excludes e.g. panels QRect availRect = effects->clientArea( PlacementArea, QPoint( 0, 0 ), effects->currentDesktop()); // Following code is taken from Kompose 0.5.4, src/komposelayout.cpp int spacing = 10; int rows, columns; float parentRatio = availRect.width() / (float)availRect.height(); // Use more columns than rows when parent's width > parent's height if ( parentRatio > 1 ) { columns = (int)ceil( sqrt(windowlist.count()) ); rows = (int)ceil( (double)windowlist.count() / (double)columns ); } else { rows = (int)ceil( sqrt(windowlist.count()) ); columns = (int)ceil( (double)windowlist.count() / (double)rows ); } kDebug() << k_funcinfo << "Using " << rows << " rows & " << columns << " columns for " << windowlist.count() << " clients" << endl; // Calculate width & height int w = (availRect.width() - (columns+1) * spacing ) / columns; int h = (availRect.height() - (rows+1) * spacing ) / rows; EffectWindowList::iterator it( windowlist.begin() ); QList geometryRects; QList maxRowHeights; // Process rows for ( int i=0; i 0 ) { usableW = w + addW; } } if ( ratio == -1 ) { widgetw = w; widgeth = h; } else { double widthForHeight = windowWidthForHeight(window, usableH); double heightForWidth = windowHeightForWidth(window, usableW); if ( (ratio >= 1.0 && heightForWidth <= usableH) || (ratio < 1.0 && widthForHeight > usableW) ) { widgetw = usableW; widgeth = (int)heightForWidth; } else if ( (ratio < 1.0 && widthForHeight <= usableW) || (ratio >= 1.0 && heightForWidth > usableH) ) { widgeth = usableH; widgetw = (int)widthForHeight; } } // Set the Widget's size int alignmentXoffset = 0; int alignmentYoffset = 0; if ( i==0 && h > widgeth ) alignmentYoffset = h - widgeth; if ( j==0 && w > widgetw ) alignmentXoffset = w - widgetw; QRect geom( availRect.x() + j * (w + spacing) + spacing + alignmentXoffset + xOffsetFromLastCol, availRect.y() + i * (h + spacing) + spacing + alignmentYoffset, widgetw, widgeth ); geometryRects.append(geom); // Set the x offset for the next column if (alignmentXoffset==0) xOffsetFromLastCol += widgetw-w; if (maxHeightInRow < widgeth) maxHeightInRow = widgeth; } maxRowHeights.append(maxHeightInRow); } int topOffset = 0; for( int i = 0; i < rows; i++ ) { for( int j = 0; j < columns; j++ ) { int pos = i*columns + j; if(pos >= windowlist.count()) break; EffectWindow* window = windowlist[pos]; QRect geom = geometryRects[pos]; geom.setY( geom.y() + topOffset ); mWindowData[window].area = geom; mWindowData[window].scale = geom.width() / (float)window->width(); mWindowData[window].hover = 0.0f; kDebug() << k_funcinfo << "Window '" << window->caption() << "' gets moved to (" << mWindowData[window].area.left() << "; " << mWindowData[window].area.right() << "), scale: " << mWindowData[window].scale << endl; } if ( maxRowHeights[i]-h > 0 ) topOffset += maxRowHeights[i]-h; } } void PresentWindowsEffect::calculateWindowTransformationsClosest(EffectWindowList windowlist) { QRect area = effects->clientArea( PlacementArea, QPoint( 0, 0 ), effects->currentDesktop()); int columns = int( ceil( sqrt( windowlist.count()))); int rows = int( ceil( windowlist.count() / double( columns ))); foreach( EffectWindow* w, windowlist ) mWindowData[ w ].slot = -1; for(;;) { // Assign each window to the closest available slot assignSlots( area, columns, rows ); // Leave only the closest window in each slot, remove further conflicts getBestAssignments(); bool all_assigned = true; foreach( EffectWindow* w, windowlist ) if( mWindowData[ w ].slot == -1 ) { all_assigned = false; break; } if( all_assigned ) break; // ok } int slotwidth = area.width() / columns; int slotheight = area.height() / rows; for( DataHash::Iterator it = mWindowData.begin(); it != mWindowData.end(); ++it ) { QRect geom( area.x() + ((*it).slot % columns ) * slotwidth, area.y() + ((*it).slot / columns ) * slotheight, slotwidth, slotheight ); geom.adjust( 10, 10, -10, -10 ); // borders float scale; EffectWindow* w = it.key(); if( geom.width() / float( w->width()) < geom.height() / float( w->height())) { // center vertically scale = geom.width() / float( w->width()); geom.moveTop( geom.top() + ( geom.height() - int( w->height() * scale )) / 2 ); geom.setHeight( int( w->height() * scale )); } else { // center horizontally scale = geom.height() / float( w->height()); geom.moveLeft( geom.left() + ( geom.width() - int( w->width() * scale )) / 2 ); geom.setWidth( int( w->width() * scale )); } (*it).area = geom; (*it).scale = scale; (*it).hover = 0; } } void PresentWindowsEffect::assignSlots( const QRect& area, int columns, int rows ) { QVector< bool > taken; taken.fill( false, columns * rows ); foreach( const WindowData& d, mWindowData ) { if( d.slot != -1 ) taken[ d.slot ] = true; } int slotwidth = area.width() / columns; int slotheight = area.height() / rows; for( DataHash::Iterator it = mWindowData.begin(); it != mWindowData.end(); ++it ) { if( (*it).slot != -1 ) continue; // it already has a slot QPoint pos = it.key()->geometry().center(); if( pos.x() < area.left()) pos.setX( area.left()); if( pos.x() > area.right()) pos.setX( area.right()); if( pos.y() < area.top()) pos.setY( area.top()); if( pos.y() > area.bottom()) pos.setY( area.bottom()); int distance = INT_MAX; for( int x = 0; x < columns; ++x ) for( int y = 0; y < rows; ++y ) { int slot = x + y * columns; if( taken[ slot ] ) continue; int xdiff = pos.x() - ( area.x() + slotwidth * x + slotwidth / 2 ); // slotwidth/2 for center int ydiff = pos.y() - ( area.y() + slotheight * y + slotheight / 2 ); int dist = int( sqrt( xdiff * xdiff + ydiff * ydiff )); if( dist < distance ) { distance = dist; (*it).slot = slot; (*it).slot_distance = distance; } } } } void PresentWindowsEffect::getBestAssignments() { for( DataHash::Iterator it1 = mWindowData.begin(); it1 != mWindowData.end(); ++it1 ) { for( DataHash::ConstIterator it2 = mWindowData.begin(); it2 != mWindowData.end(); ++it2 ) { if( it1.key() != it2.key() && (*it1).slot == (*it2).slot && (*it1).slot_distance >= (*it2).slot_distance ) { (*it1).slot = -1; } } } } bool PresentWindowsEffect::canRearrangeClosest(EffectWindowList windowlist) { QRect area = effects->clientArea( PlacementArea, QPoint( 0, 0 ), effects->currentDesktop()); int columns = int( ceil( sqrt( windowlist.count()))); int rows = int( ceil( windowlist.count() / double( columns ))); int old_columns = int( ceil( sqrt( mWindowData.count()))); int old_rows = int( ceil( mWindowData.count() / double( columns ))); return old_columns != columns || old_rows != rows; } bool PresentWindowsEffect::borderActivated( ElectricBorder border ) { if( border == borderActivate && !mActivated ) { toggleActive(); return true; } if( border == borderActivateAll && !mActivated ) { toggleActiveAllDesktops(); return true; } return false; } void PresentWindowsEffect::grabbedKeyboardEvent( QKeyEvent* e ) { if( e->type() != QEvent::KeyPress ) return; if( e->key() == Qt::Key_Escape ) { setActive( false ); return; } if( e->key() == Qt::Key_Backspace ) { if( !windowFilter.isEmpty()) { windowFilter.remove( windowFilter.length() - 1, 1 ); updateFilterTexture(); rearrangeWindows(); } return; } if( e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter ) { if( mHoverWindow != NULL ) { effects->activateWindow( mHoverWindow ); setActive( false ); return; } if( mWindowData.count() == 1 ) // only one window shown { effects->activateWindow( mWindowData.begin().key()); setActive( false ); } return; } if( !e->text().isEmpty()) { windowFilter.append( e->text()); updateFilterTexture(); rearrangeWindows(); return; } } void PresentWindowsEffect::discardFilterTexture() { #ifdef HAVE_OPENGL delete filterTexture; filterTexture = NULL; #endif } void PresentWindowsEffect::updateFilterTexture() { #ifdef HAVE_OPENGL discardFilterTexture(); if( windowFilter.isEmpty()) return; QFont font; font.setPointSize( font.pointSize() * 2 ); font.setBold( true ); QRect rect = QFontMetrics( font ).boundingRect( windowFilter ); const int border = 10; rect.adjust( -border, -border, border, border ); QRect area = effects->clientArea( PlacementArea, QPoint( 0, 0 ), effects->currentDesktop()); QImage im( rect.width(), rect.height(), QImage::Format_ARGB32 ); QColor col = QPalette().highlight(); col.setAlpha( 128 ); // 0.5 im.fill( col.rgba()); QPainter p( &im ); p.setFont( font ); p.setPen( QPalette().highlightedText()); p.drawText( -rect.topLeft(), windowFilter ); p.end(); filterTexture = new GLTexture( im ); filterTextureRect = QRect( area.x() + ( area.width() - rect.width()) / 2, area.y() + ( area.height() - rect.height()) / 2, rect.width(), rect.height()); effects->addRepaint( filterTextureRect ); #endif } } // namespace #include "presentwindows.moc"