diff --git a/README.md b/README.md index 9b03792..afd70b3 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,11 @@ On-demand tiling for Openbox, Xfce and other [EWMH Compliant Window Managers](ht ### Features - Workspace based tiling. You can enable tiling in one workspace and leave others untouched. -- Ships with two simple tiling layouts (Vertical & Horizontal) +- Ships with four simple tiling layouts: + Vertical, Horizontal, Square, and FullScreen - Customizable gap between tiling windows. - Autodetection of panels and docks. +- Support for multiple monitors (Square Layout) ### Installation diff --git a/config.go b/config.go index 81a3310..e78de55 100644 --- a/config.go +++ b/config.go @@ -17,7 +17,9 @@ type cfg struct { WindowsToIgnore []string `toml:"ignore"` Gap int Proportion float64 - HideDecor bool `toml:"remove_decorations"` + HideDecor bool `toml:"remove_decorations"` + MMRegions [][4]int `toml:"multi_region_geometry"` + DefaultLayout uint `toml:"default_layout"` } func init() { @@ -56,7 +58,16 @@ func configFilePath() string { return filepath.Join(configFolderPath(), "config.toml") } -var defaultConfig = `# Window decorations will be removed when tiling if set to true +var defaultConfig = `## General Config + +# Startup Layout - preferred layout to begin with +# 0 - Vertical +# 1 - Horizontal +# 2 - Square +# 3 - Full Screen +default_layout = 0 + +# Window decorations will be removed when tiling if set to true remove_decorations = false # Zentile will ignore windows added to this list. @@ -70,6 +81,27 @@ gap = 5 # How much to increment the master area size. proportion = 0.1 + +## Square Layout Config +# Multiple Monitor Support (optional) +# +# You can use this to describe one or more areas where you want windows to be +# handled separately. This is most commonly because you have multiple monitors +# displaying portions of, but not the full, workarea dimensions. +# +# This is a list of region descriptions in the form (x, y, width, height) +# where x and y represent the Top-Left corner of the region within the workarea +# +# Here is an example with two monitors: one portrait and one landscape, with the +# landscape display at a vertical offset of 630 pixels and to the right edge of the +# portrait display + +# multi_region_geometry = [ +# [0, 0, 1080, 1920], +# [1080, 630, 1920, 1080] +#] + + [keybindings] # key sequences can have zero or more modifiers and exactly one key. # example: Control-Shift-t has two modifiers and one key. diff --git a/keybinding.go b/keybinding.go index ee55967..c4eeb15 100644 --- a/keybinding.go +++ b/keybinding.go @@ -29,6 +29,9 @@ func bindKeys(t *tracker) { k.bind("tile", func() { ws := workspaces[state.CurrentDesk] + if !ws.IsTiling { + ws.activeLayoutNum = Config.DefaultLayout % uint(len(ws.layouts)) + } ws.IsTiling = true ws.Tile() }) diff --git a/state/state.go b/state/state.go index d02d441..3ab6342 100644 --- a/state/state.go +++ b/state/state.go @@ -93,5 +93,6 @@ func WorkAreaDimensions(num uint) (x, y, width, height int) { y = w.Y width = int(w.Width) height = int(w.Height) + // log.Info("workArea ", num, " - X: ", x, " Y: ", y, " W: ", width, " H: ", height) return } diff --git a/store.go b/store.go index a3b0472..124dcd0 100644 --- a/store.go +++ b/store.go @@ -67,9 +67,28 @@ func (st *Store) DecreaseMaster() { } func (st *Store) MakeMaster(c Client) { + if len(st.masters) > 0 && st.masters[0].window.Id == c.window.Id { + // Mastering the first master demotes it + s := st.masters[0] + slen := len(st.slaves) + if slen == 0 { + st.slaves = []Client{s} + st.masters = st.masters[1:] + } else { + st.masters[0], st.slaves[slen-1] = st.slaves[slen-1], st.masters[0] + } + return + } + for i, slave := range st.slaves { if slave.window.Id == c.window.Id { - st.masters[0], st.slaves[i] = st.slaves[i], st.masters[0] + if len(st.masters) > 0 { + st.masters[0], st.slaves[i] = st.slaves[i], st.masters[0] + } else { + st.masters = []Client{st.slaves[i]} + st.slaves = append(st.slaves[:i], st.slaves[i+1:]...) + } + break } } } diff --git a/vertical_horizonal_layout.go b/vertical_horizonal_layout.go index ce3bb36..8bb0b3a 100644 --- a/vertical_horizonal_layout.go +++ b/vertical_horizonal_layout.go @@ -1,6 +1,8 @@ package main import ( + "math" + "github.com/blrsn/zentile/state" log "github.com/sirupsen/logrus" ) @@ -98,3 +100,128 @@ func (l *HorizontalLayout) Do() { state.X.Conn().Sync() } + +type SquareLayout struct { + *VertHorz +} + +func (l *SquareLayout) Do() { + // intended for lots of small windows + log.Info("Switching to Square Layout") + wx, wy, ww, wh := state.WorkAreaDimensions(l.WorkspaceNum) + + gap := Config.Gap + + // concatenating all the window clients into a single list + // The master window proportional zoom is not supported in this layout. + // Selecting a window as a master using the hotkey has the effect of + // swapping it with whatever window was currently in the number + // one (top-left) position. + allClients := append(l.masters, l.slaves...) + + // sub-regions of the main work area (to describe multiple monitors) + // regions is a slice of (x,y,width,height) arrays + // clients get divided evenly among regions then regions are + // rendered serially + var regions [][4]int = Config.MMRegions + + if len(regions) == 0 { + regions = append(regions, [4]int{wx, wy, ww, wh}) + } + + nregions := len(regions) + segsize := len(allClients) / nregions + + for i := 0; i < nregions; i += 1 { + region := regions[i] + rx := region[0] + ry := region[1] + rw := region[2] + rh := region[3] + + var regionClients []Client + if i+1 == nregions { + regionClients = allClients[i*segsize:] + } else { + regionClients = allClients[i*segsize : (i+1)*segsize] + } + csize := len(regionClients) + + if csize == 0 { + continue + } + cols := int(math.Floor(math.Sqrt(float64(csize)))) + rows := cols // default to perfect square + + // cols^2 + 2*cols + 1 === (cols + 1)^2 + extras := int(math.Ceil(float64(csize)/float64(cols))) - cols // 0..2 + + // if taller than wide, add additional rows first + // if wider than tall, add additional columns first + if extras > 0 { + if rh >= rw { + rows = rows + 1 + } else { + cols = cols + 1 + } + } + if extras == 2 { + if rh >= rw { + cols = cols + 1 + } else { + rows = rows + 1 + } + } + + colsize := rw/cols - gap + rowsize := rh/rows - gap + + padx := 0 + pady := 0 + + // here is an algo for auto-padding (gap becomes minimum pad) + // i ended up not liking it and figured it would add too + // much complexity to the config, but here it is... + + // if rowsize < colsize && cols > 1 { + // padx = (colsize - rowsize) * cols / (cols - 1) + // colsize = rowsize + // for padx > colsize { + // colsize = colsize * 3 / 2 + // padx = (rw - colsize*cols) / (cols - 1) + // } + // } else if rowsize > colsize && rows > 1 { + // pady = (rowsize - colsize) * rows / (rows - 1) + // rowsize = colsize + // for pady > rowsize { + // rowsize = rowsize * 3 / 2 + // pady = (rh - rowsize*rows) / (rows - 1) + // } + // } + + mx := rx + my := ry + + log.Info("cols: ", cols, " rows: ", rows, " colsize: ", colsize, " rowsize: ", rowsize, " padx: ", padx, " pady: ", pady) + + currcol := 1 + for _, c := range regionClients { + if Config.HideDecor { + c.UnDecorate() + } + + log.Info("Moving ", c.name(), ": ", " X: ", mx, " Y: ", my) + c.MoveResize(mx, my, colsize, rowsize) + + mx = mx + colsize + padx + gap + currcol = currcol + 1 + if currcol > cols { + mx = rx + my = my + rowsize + pady + gap + currcol = 1 + } + } + } + + state.X.Conn().Sync() +} diff --git a/workspace.go b/workspace.go index 990978b..7ed0456 100644 --- a/workspace.go +++ b/workspace.go @@ -38,6 +38,10 @@ func createLayouts(workspaceNum uint) []Layout { Proportion: 0.5, WorkspaceNum: workspaceNum, }}, + &SquareLayout{&VertHorz{ + Store: buildStore(), + WorkspaceNum: workspaceNum, + }}, &FullScreen{ Store: buildStore(), WorkspaceNum: workspaceNum,