2025-02-09 23:16:18 +01:00
/ * *
* V - Shell ( Vertical Workspaces )
* workspace . js
*
* @ author GdH < G - dH @ github . com >
2025-02-09 23:18:13 +01:00
* @ copyright 2022 - 2024
2025-02-09 23:16:18 +01:00
* @ license GPL - 3.0
*
* /
'use strict' ;
import St from 'gi://St' ;
import Graphene from 'gi://Graphene' ;
import * as Main from 'resource:///org/gnome/shell/ui/main.js' ;
import * as Workspace from 'resource:///org/gnome/shell/ui/workspace.js' ;
import * as Params from 'resource:///org/gnome/shell/misc/params.js' ;
import * as Util from 'resource:///org/gnome/shell/misc/util.js' ;
let Me ;
let opt ;
let WINDOW _PREVIEW _MAXIMUM _SCALE = 0.95 ;
export const WorkspaceModule = class {
constructor ( me ) {
Me = me ;
opt = Me . opt ;
this . _firstActivation = true ;
this . moduleEnabled = false ;
this . _overrides = null ;
}
cleanGlobals ( ) {
Me = null ;
opt = null ;
}
update ( reset ) {
this . moduleEnabled = opt . get ( 'workspaceModule' ) ;
const conflict = false ;
reset = reset || ! this . moduleEnabled || conflict ;
// don't touch the original code if module disabled
if ( reset && ! this . _firstActivation ) {
this . _disableModule ( ) ;
} else if ( ! reset ) {
this . _firstActivation = false ;
this . _activateModule ( ) ;
}
if ( reset && this . _firstActivation )
console . debug ( ' WorkspaceModule - Keeping untouched' ) ;
}
_activateModule ( ) {
if ( ! this . _overrides )
this . _overrides = new Me . Util . Overrides ( ) ;
this . _overrides . addOverride ( 'WorkspaceBackground' , Workspace . WorkspaceBackground . prototype , WorkspaceBackground ) ;
// fix overlay base for Vertical Workspaces
this . _overrides . addOverride ( 'WorkspaceLayout' , Workspace . WorkspaceLayout . prototype , WorkspaceLayout ) ;
console . debug ( ' WorkspaceModule - Activated' ) ;
}
_disableModule ( ) {
if ( this . _overrides )
this . _overrides . removeAll ( ) ;
this . _overrides = null ;
console . debug ( ' WorkspaceModule - Disabled' ) ;
}
setWindowPreviewMaxScale ( scale ) {
WINDOW _PREVIEW _MAXIMUM _SCALE = scale ;
}
} ;
// workaround for upstream bug (that is not that invisible in default shell)
// smaller window cannot be scaled below 0.95 (WINDOW_PREVIEW_MAXIMUM_SCALE)
// when its target scale for exposed windows view (workspace state 1) is bigger than the scale needed for ws state 0.
// in workspace state 0 where windows are not spread and window scale should follow workspace scale,
// this window follows proper top left corner position, but doesn't scale with the workspace
// so it looks bad and the window can exceed border of the workspace
// extremely annoying in OVERVIEW_MODE 1 with single smaller window on the workspace, also affects appGrid transition animation
// disadvantage of following workaround - the WINDOW_PREVIEW_MAXIMUM_SCALE value is common for every workspace,
// on multi-monitor system can be visible unwanted scaling of windows on workspace in WORKSPACE_MODE 0 (windows not spread)
// when leaving overview while any other workspace is in the WORKSPACE_MODE 1.
const WorkspaceLayout = {
// injection to _init()
after _ _init ( ) {
if ( opt . OVERVIEW _MODE !== 1 )
WINDOW _PREVIEW _MAXIMUM _SCALE = 0.95 ;
if ( opt . OVERVIEW _MODE === 1 ) {
this . _stateAdjustment . connect ( 'notify::value' , ( ) => {
// scale 0.1 for window state 0 just needs to be smaller then possible scale of any window in spread view
const scale = this . _stateAdjustment . value ? 0.95 : 0.1 ;
if ( scale !== WINDOW _PREVIEW _MAXIMUM _SCALE ) {
WINDOW _PREVIEW _MAXIMUM _SCALE = scale ;
// when transition to ws state 1 (WINDOW_PICKER) begins, replace the constant with the original one
// and force recalculation of the target layout, so the transition will be smooth
this . _needsLayout = true ;
}
} ) ;
}
} ,
// this fixes wrong size and position calculation of window clones while moving overview to the next (+1) workspace if vertical ws orientation is enabled in GS
_adjustSpacingAndPadding ( rowSpacing , colSpacing , containerBox ) {
if ( this . _sortedWindows . length === 0 )
return [ rowSpacing , colSpacing , containerBox ] ;
// All of the overlays have the same chrome sizes,
// so just pick the first one.
const window = this . _sortedWindows [ 0 ] ;
const [ topOversize , bottomOversize ] = window . chromeHeights ( ) ;
const [ leftOversize , rightOversize ] = window . chromeWidths ( ) ;
const oversize = Math . max ( topOversize , bottomOversize , leftOversize , rightOversize ) ;
if ( rowSpacing !== null )
rowSpacing += oversize ;
if ( colSpacing !== null )
colSpacing += oversize ;
if ( containerBox ) {
const vertical = global . workspaceManager . layout _rows === - 1 ;
const monitor = Main . layoutManager . monitors [ this . _monitorIndex ] ;
const bottomPoint = new Graphene . Point3D ( ) ;
if ( vertical )
bottomPoint . x = containerBox . x2 ;
else
bottomPoint . y = containerBox . y2 ;
const transformedBottomPoint =
this . _container . apply _transform _to _point ( bottomPoint ) ;
const bottomFreeSpace = vertical
? ( monitor . x + monitor . height ) - transformedBottomPoint . x
: ( monitor . y + monitor . height ) - transformedBottomPoint . y ;
const [ , bottomOverlap ] = window . overlapHeights ( ) ;
if ( ( bottomOverlap + oversize ) > bottomFreeSpace && ! vertical )
containerBox . y2 -= ( bottomOverlap + oversize ) - bottomFreeSpace ;
}
return [ rowSpacing , colSpacing , containerBox ] ;
} ,
_createBestLayout ( area ) {
const [ rowSpacing , columnSpacing ] =
this . _adjustSpacingAndPadding ( this . _spacing , this . _spacing , null ) ;
// We look for the largest scale that allows us to fit the
// largest row/tallest column on the workspace.
this . _layoutStrategy = new UnalignedLayoutStrategy ( {
monitor : Main . layoutManager . monitors [ this . _monitorIndex ] ,
rowSpacing ,
columnSpacing ,
} ) ;
let lastLayout = null ;
let lastNumColumns = - 1 ;
let lastScale = 0 ;
let lastSpace = 0 ;
for ( let numRows = 1 ; ; numRows ++ ) {
const numColumns = Math . ceil ( this . _sortedWindows . length / numRows ) ;
// If adding a new row does not change column count just stop
// (for instance: 9 windows, with 3 rows -> 3 columns, 4 rows ->
// 3 columns as well => just use 3 rows then)
if ( numColumns === lastNumColumns )
break ;
const layout = this . _layoutStrategy . computeLayout ( this . _sortedWindows , {
numRows ,
} ) ;
const [ scale , space ] = this . _layoutStrategy . computeScaleAndSpace ( layout , area ) ;
if ( lastLayout && ! this . _isBetterScaleAndSpace ( lastScale , lastSpace , scale , space ) )
break ;
lastLayout = layout ;
lastNumColumns = numColumns ;
lastScale = scale ;
lastSpace = space ;
}
return lastLayout ;
} ,
} ;
class UnalignedLayoutStrategy extends Workspace . LayoutStrategy {
_newRow ( ) {
// Row properties:
//
// * x, y are the position of row, relative to area
//
// * width, height are the scaled versions of fullWidth, fullHeight
//
// * width also has the spacing in between windows. It's not in
// fullWidth, as the spacing is constant, whereas fullWidth is
// meant to be scaled
//
// * neither height/fullHeight have any sort of spacing or padding
return {
x : 0 , y : 0 ,
width : 0 , height : 0 ,
fullWidth : 0 , fullHeight : 0 ,
windows : [ ] ,
} ;
}
// Computes and returns an individual scaling factor for @window,
// to be applied in addition to the overall layout scale.
_computeWindowScale ( window ) {
// Since we align windows next to each other, the height of the
// thumbnails is much more important to preserve than the width of
// them, so two windows with equal height, but maybe differering
// widths line up.
let ratio = window . boundingBox . height / this . _monitor . height ;
// The purpose of this manipulation here is to prevent windows
// from getting too small. For something like a calculator window,
// we need to bump up the size just a bit to make sure it looks
// good. We'll use a multiplier of 1.5 for this.
// Map from [0, 1] to [1.5, 1]
return Util . lerp ( 1.5 , 1 , ratio ) ;
}
_computeRowSizes ( layout ) {
let { rows , scale } = layout ;
for ( let i = 0 ; i < rows . length ; i ++ ) {
let row = rows [ i ] ;
row . width = row . fullWidth * scale + ( row . windows . length - 1 ) * this . _columnSpacing ;
row . height = row . fullHeight * scale ;
}
}
_keepSameRow ( row , window , width , idealRowWidth ) {
if ( row . fullWidth + width <= idealRowWidth )
return true ;
let oldRatio = row . fullWidth / idealRowWidth ;
let newRatio = ( row . fullWidth + width ) / idealRowWidth ;
if ( Math . abs ( 1 - newRatio ) < Math . abs ( 1 - oldRatio ) )
return true ;
return false ;
}
_sortRow ( row ) {
// Sort windows horizontally to minimize travel distance.
// This affects in what order the windows end up in a row.
row . windows . sort ( ( a , b ) => a . windowCenter . x - b . windowCenter . x ) ;
}
computeLayout ( windows , layoutParams ) {
layoutParams = Params . parse ( layoutParams , {
numRows : 0 ,
} ) ;
if ( layoutParams . numRows === 0 )
throw new Error ( ` ${ this . constructor . name } : No numRows given in layout params ` ) ;
const numRows = layoutParams . numRows ;
let rows = [ ] ;
let totalWidth = 0 ;
for ( let i = 0 ; i < windows . length ; i ++ ) {
let window = windows [ i ] ;
let s = this . _computeWindowScale ( window ) ;
totalWidth += window . boundingBox . width * s ;
}
let idealRowWidth = totalWidth / numRows ;
// Sort windows vertically to minimize travel distance.
// This affects what rows the windows get placed in.
let sortedWindows = windows . slice ( ) ;
sortedWindows . sort ( ( a , b ) => a . windowCenter . y - b . windowCenter . y ) ;
let windowIdx = 0 ;
for ( let i = 0 ; i < numRows ; i ++ ) {
let row = this . _newRow ( ) ;
rows . push ( row ) ;
for ( ; windowIdx < sortedWindows . length ; windowIdx ++ ) {
let window = sortedWindows [ windowIdx ] ;
let s = this . _computeWindowScale ( window ) ;
let width = window . boundingBox . width * s ;
let height = window . boundingBox . height * s ;
row . fullHeight = Math . max ( row . fullHeight , height ) ;
// either new width is < idealWidth or new width is nearer from idealWidth then oldWidth
if ( this . _keepSameRow ( row , window , width , idealRowWidth ) || ( i === numRows - 1 ) ) {
row . windows . push ( window ) ;
row . fullWidth += width ;
} else {
break ;
}
}
}
let gridHeight = 0 ;
let maxRow ;
for ( let i = 0 ; i < numRows ; i ++ ) {
let row = rows [ i ] ;
this . _sortRow ( row ) ;
if ( ! maxRow || row . fullWidth > maxRow . fullWidth )
maxRow = row ;
gridHeight += row . fullHeight ;
}
return {
numRows ,
rows ,
maxColumns : maxRow . windows . length ,
gridWidth : maxRow . fullWidth ,
gridHeight ,
} ;
}
computeScaleAndSpace ( layout , area ) {
let hspacing = ( layout . maxColumns - 1 ) * this . _columnSpacing ;
let vspacing = ( layout . numRows - 1 ) * this . _rowSpacing ;
let spacedWidth = area . width - hspacing ;
let spacedHeight = area . height - vspacing ;
let horizontalScale = spacedWidth / layout . gridWidth ;
let verticalScale = spacedHeight / layout . gridHeight ;
// Thumbnails should be less than 70% of the original size
let scale = Math . min (
horizontalScale , verticalScale , WINDOW _PREVIEW _MAXIMUM _SCALE ) ;
let scaledLayoutWidth = layout . gridWidth * scale + hspacing ;
let scaledLayoutHeight = layout . gridHeight * scale + vspacing ;
let space = ( scaledLayoutWidth * scaledLayoutHeight ) / ( area . width * area . height ) ;
layout . scale = scale ;
return [ scale , space ] ;
}
computeWindowSlots ( layout , area ) {
this . _computeRowSizes ( layout ) ;
let { rows , scale } = layout ;
let slots = [ ] ;
// Do this in three parts.
let heightWithoutSpacing = 0 ;
for ( let i = 0 ; i < rows . length ; i ++ ) {
let row = rows [ i ] ;
heightWithoutSpacing += row . height ;
}
let verticalSpacing = ( rows . length - 1 ) * this . _rowSpacing ;
let additionalVerticalScale = Math . min ( 1 , ( area . height - verticalSpacing ) / heightWithoutSpacing ) ;
// keep track how much smaller the grid becomes due to scaling
// so it can be centered again
let compensation = 0 ;
let y = 0 ;
for ( let i = 0 ; i < rows . length ; i ++ ) {
let row = rows [ i ] ;
// If this window layout row doesn't fit in the actual
// geometry, then apply an additional scale to it.
let horizontalSpacing = ( row . windows . length - 1 ) * this . _columnSpacing ;
let widthWithoutSpacing = row . width - horizontalSpacing ;
let additionalHorizontalScale = Math . min ( 1 , ( area . width - horizontalSpacing ) / widthWithoutSpacing ) ;
if ( additionalHorizontalScale < additionalVerticalScale ) {
row . additionalScale = additionalHorizontalScale ;
// Only consider the scaling in addition to the vertical scaling for centering.
compensation += ( additionalVerticalScale - additionalHorizontalScale ) * row . height ;
} else {
row . additionalScale = additionalVerticalScale ;
// No compensation when scaling vertically since centering based on a too large
// height would undo what vertical scaling is trying to achieve.
}
row . x = area . x + ( Math . max ( area . width - ( widthWithoutSpacing * row . additionalScale + horizontalSpacing ) , 0 ) / 2 ) ;
row . y = area . y + ( Math . max ( area . height - ( heightWithoutSpacing + verticalSpacing ) , 0 ) / 2 ) + y ;
y += row . height * row . additionalScale + this . _rowSpacing ;
}
compensation /= 2 ;
for ( let i = 0 ; i < rows . length ; i ++ ) {
const row = rows [ i ] ;
const rowY = row . y + compensation ;
const rowHeight = row . height * row . additionalScale ;
let x = row . x ;
for ( let j = 0 ; j < row . windows . length ; j ++ ) {
let window = row . windows [ j ] ;
let s = scale * this . _computeWindowScale ( window ) * row . additionalScale ;
let cellWidth = window . boundingBox . width * s ;
let cellHeight = window . boundingBox . height * s ;
s = Math . min ( s , WINDOW _PREVIEW _MAXIMUM _SCALE ) ;
let cloneWidth = window . boundingBox . width * s ;
const cloneHeight = window . boundingBox . height * s ;
let cloneX = x + ( cellWidth - cloneWidth ) / 2 ;
let cloneY ;
// If there's only one row, align windows vertically centered inside the row
if ( rows . length === 1 )
cloneY = rowY + ( rowHeight - cloneHeight ) / 2 ;
// If there are multiple rows, align windows to the bottom edge of the row
else
cloneY = rowY + rowHeight - cellHeight ;
// Align with the pixel grid to prevent blurry windows at scale = 1
cloneX = Math . floor ( cloneX ) ;
cloneY = Math . floor ( cloneY ) ;
slots . push ( [ cloneX , cloneY , cloneWidth , cloneHeight , window ] ) ;
x += cellWidth + this . _columnSpacing ;
}
}
return slots ;
}
}
const WorkspaceBackground = {
_updateBorderRadius ( value = false ) {
// don't round already rounded corners during exposing windows
if ( value === false && opt . OVERVIEW _MODE === 1 )
return ;
const { scaleFactor } = St . ThemeContext . get _for _stage ( global . stage ) ;
const cornerRadius = scaleFactor * opt . WS _PREVIEW _BG _RADIUS ;
const backgroundContent = this . _bgManager . backgroundActor . content ;
value = value !== false
? value
: this . _stateAdjustment . value ;
backgroundContent . rounded _clip _radius =
Util . lerp ( 0 , cornerRadius , value ) ;
} ,
} ;