Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,21 +200,25 @@ You can create your own custom layout by specifying which views you'd like
to see, and where they go. The basic window layout supports eight "zones",
which are laid out as follows:

```
+---------------+--------------+
| zone1 zone2 | zone3 zone4 |
+ + +
| zone5 zone6 | zone7 zone8 |
+---------------+--------------+
```

If a zone has free space below it or to the right of it, it will try to use it.
Stretching downwards takes precedence over stretching rightwards.
E.g. suppose your layout is only non-empty in zones 1, 4, 5, 6 and 7:

```
+---------------+--------------+
| zone1 | zone4 |
+ + +
| zone5 zone6 | zone7 |
+---------------+--------------+
```

Then zone1 will stretch right-wards to make a three-zone view. Zone4 will stretch
downwards to make a long two-zone view.
Expand Down
75 changes: 56 additions & 19 deletions spikeinterface_gui/backend_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@

from .viewlist import possible_class_views
from .layout_presets import get_layout_description
from .utils_global import get_size_bottom_row, get_size_top_row

from .utils_global import fill_unnecessary_space, get_present_zones_in_half_of_layout
# Used by views to emit/trigger signals
class SignalNotifier(param.Parameterized):
spike_selection_changed = param.Event()
Expand Down Expand Up @@ -283,26 +282,64 @@ def create_main_layout(self):
allow_drag=False,
)

gs = self.make_half_layout(gs, ['zone1', 'zone2', 'zone5', 'zone6'], layout_zone, 0)
gs = self.make_half_layout(gs, ['zone3', 'zone4', 'zone7', 'zone8'], layout_zone, 2)
gs = self.make_half_layout(gs, layout_zone, "left")
gs = self.make_half_layout(gs, layout_zone, "right")

self.main_layout = gs

def make_half_layout(self, gs, all_zones, layout_zone, shift):

is_zone = [(layout_zone.get(zone) is not None) and (len(layout_zone.get(zone)) > 0) for zone in all_zones]
is_zone_array = np.reshape(is_zone, (2,2))
original_zone_array = copy(is_zone_array)

for zone_index, zone_name in enumerate(all_zones):
row = zone_index // 2
col = zone_index % 2
if row == 0:
num_rows, num_cols = get_size_top_row(row, col, is_zone_array, original_zone_array)
elif row == 1:
num_rows, num_cols = get_size_bottom_row(row, col, is_zone_array, original_zone_array)
if num_rows > 0 and num_cols > 0:
gs[slice(row, row + num_rows), slice(col+shift,col+num_cols+shift)] = layout_zone.get(zone_name)
def make_half_layout(self, gs, layout_zone, left_or_right):
"""
Function contains the logic for the greedy layout. Given the 2x2 box of zones

1 2 3 4
5 6 or 7 8

Then depending on which zones are non-zero, a different layout is generated using splits.

The zone indices in the second box (34,78) are equal to the zone indices first box (12,56)
shifted by 2. We take advantage of this fact.
"""

shift = 0 if left_or_right == "left" else 2

layout_zone = fill_unnecessary_space(layout_zone, shift)
present_zones = get_present_zones_in_half_of_layout(layout_zone, shift)

# `fill_unnecessary_space` ensures that zone{1+shift} always exists
if present_zones == set([f'zone{1+shift}']):
gs[0,0] = layout_zone.get(f'zone{1+shift}')

# Layouts with two non-zero zones
if present_zones == set([f'zone{1+shift}', f'zone{2+shift}']):
gs[slice(0, 1), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
gs[slice(0, 1), slice(1+shift,2+shift)] = layout_zone.get(f'zone{2+shift}')
elif present_zones == set([f'zone{1+shift}', f'zone{5+shift}']):
gs[slice(0, 1), slice(0+shift,2+shift)] = layout_zone.get(f'zone{1+shift}')
gs[slice(1, 2), slice(0+shift,2+shift)] = layout_zone.get(f'zone{5+shift}')
elif present_zones == set([f'zone{1+shift}', f'zone{6+shift}']):
gs[slice(0, 1), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
gs[slice(0, 1), slice(1+shift,2+shift)] = layout_zone.get(f'zone{6+shift}')

# Layouts with three non-zero zones
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}']):
gs[slice(0, 1), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
gs[slice(0, 2), slice(1+shift,2+shift)] = layout_zone.get(f'zone{2+shift}')
gs[slice(1, 2), slice(0+shift,1+shift)] = layout_zone.get(f'zone{5+shift}')
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{6+shift}']):
gs[slice(0, 2), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
gs[slice(0, 1), slice(1+shift,2+shift)] = layout_zone.get(f'zone{2+shift}')
gs[slice(1, 2), slice(1+shift,1+shift)] = layout_zone.get(f'zone{6+shift}')
elif present_zones == set([f'zone{1+shift}', f'zone{5+shift}', f'zone{6+shift}']):
gs[slice(0, 1), slice(0+shift,2+shift)] = layout_zone.get(f'zone{1+shift}')
gs[slice(1, 2), slice(0+shift,1+shift)] = layout_zone.get(f'zone{5+shift}')
gs[slice(1, 2), slice(1+shift,2+shift)] = layout_zone.get(f'zone{6+shift}')

# Layouts with four non-zero zones
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}', f'zone{6+shift}']):
gs[slice(0, 1), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
gs[slice(0, 1), slice(1+shift,2+shift)] = layout_zone.get(f'zone{2+shift}')
gs[slice(1, 2), slice(0+shift,1+shift)] = layout_zone.get(f'zone{5+shift}')
gs[slice(1, 2), slice(1+shift,2+shift)] = layout_zone.get(f'zone{6+shift}')

return gs

Expand Down
155 changes: 61 additions & 94 deletions spikeinterface_gui/backend_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from .viewlist import possible_class_views
from .layout_presets import get_layout_description
from .utils_global import get_size_bottom_row, get_size_top_row
from .utils_global import fill_unnecessary_space, get_present_zones_in_half_of_layout

from .utils_qt import qt_style, add_stretch_to_qtoolbar

Expand Down Expand Up @@ -200,9 +200,9 @@ def create_main_layout(self):
view_names = [view_name for view_name in view_names if view_name in self.views.keys()]
widgets_zone[zone] = view_names

self.make_dock(widgets_zone, ['zone1', 'zone2', 'zone5', 'zone6'], "left", col_shift=0)
self.make_dock(widgets_zone, ['zone3', 'zone4', 'zone7', 'zone8'], "right", col_shift=2)
self.make_half_layout(widgets_zone, "left")
self.make_half_layout(widgets_zone, "right")

# make tabs
for zone, view_names in widgets_zone.items():
n = len(widgets_zone[zone])
Expand All @@ -217,103 +217,70 @@ def create_main_layout(self):
# make visible the first of each zone
self.docks[view_name0].raise_()

def make_dock(self, widgets_zone, all_zones, side_of_window, col_shift):

all_zones_array = np.transpose(np.reshape(all_zones, (2,2)))
is_zone = np.array([(widgets_zone.get(zone) is not None) and (len(widgets_zone.get(zone)) > 0) for zone in all_zones])
is_zone_array = np.reshape(is_zone, (2,2))

# If the first non-zero zero (from left to right) is on the bottom, move it up
for column_index, zones_in_columns in enumerate(is_zone_array):
if np.any(zones_in_columns):
first_is_top = zones_in_columns[0]
if not first_is_top:
top_zone = f"zone{column_index+1+col_shift}"
bottom_zone = f"zone{column_index+5+col_shift}"
widgets_zone[top_zone] = widgets_zone[bottom_zone]
widgets_zone[bottom_zone] = []
continue

is_zone = np.array([(widgets_zone.get(zone) is not None) and (len(widgets_zone.get(zone)) > 0) for zone in all_zones])
is_zone_array = np.reshape(is_zone, (2,2))
original_zone_array = copy(is_zone_array)

# First we split horizontally any columns which are two rows long.
# For later, group the zones between these splits
all_groups = []
group = []
for col_index, zones in enumerate(all_zones_array):
col = col_index % 2
is_a_zone = original_zone_array[:,col]
num_row_0, _ = get_size_top_row(0, col, is_zone_array, original_zone_array)
# this function affects is_zone_array so must be run
_, _ = get_size_bottom_row(1, col, is_zone_array, original_zone_array)

if num_row_0 == 2:
if len(group) > 0:
all_groups.append(group)
group = []
allowed_zones = zones[is_a_zone]
all_groups.append(allowed_zones)
else:
for zone in zones[is_a_zone]:
group.append(zone)

if len(group) > 0:
all_groups.append(group)

if len(all_groups) == 0:
return

first_zone = all_groups[0][0]
first_dock = widgets_zone[first_zone][0]
dock = self.docks[first_dock]
self.addDockWidget(areas[side_of_window], dock)

for group in reversed(all_groups[1:]):
digits = np.array([int(s[-1]) for s in group])
sorted_indices = np.argsort(digits)
sorted_arr = np.array(group)[sorted_indices]
view_name = widgets_zone[sorted_arr[0]][0]
dock = self.docks[view_name]
self.splitDockWidget(self.docks[first_dock], dock, orientations['horizontal'])

# Now take each sub-group, and split vertically if appropriate
new_all_groups = []
for group in all_groups:

if len(group) == 1:
# if only one in group, not need to split
continue

top_zones = [zone for zone in group if zone in ['zone1', 'zone2', 'zone3', 'zone4']]
bottom_zones = [zone for zone in group if zone in ['zone5', 'zone6', 'zone7', 'zone8']]
new_all_groups.append([top_zones, bottom_zones])
def make_half_layout(self, widgets_zone, left_or_right):
"""
Function contains the logic for the greedy layout. Given the 2x2 box of zones

if len(top_zones) > 0 and len(bottom_zones) > 0:
1 2 3 4
5 6 or 7 8

top_view_name = widgets_zone[top_zones[0]][0]
top_dock = self.docks[top_view_name]
Then depending on which zones are non-zero, a different layout is generated using splits.

bottom_view_name = widgets_zone[bottom_zones[0]][0]
bottom_dock = self.docks[bottom_view_name]
The zone indices in the second box (34,78) are equal to the zone indices first box (12,56)
shifted by 2. We take advantage of this fact.
"""

self.splitDockWidget(top_dock, bottom_dock, orientations['vertical'])
shift = 0 if left_or_right == "left" else 2

# Finally, split all the sub-sub-groups horizontally
for top_bottom_groups in new_all_groups:
for group in top_bottom_groups:

if len(group) <= 1:
# if only one in group, no need to split
continue

first_zone_name = widgets_zone[group[0]][0]
for zone in reversed(group[1:]):
zone_name = widgets_zone[zone][0]
self.splitDockWidget(self.docks[first_zone_name], self.docks[zone_name], orientations['horizontal'])
widgets_zone = fill_unnecessary_space(widgets_zone, shift)
present_zones = get_present_zones_in_half_of_layout(widgets_zone, shift)

if len(present_zones) == 0:
return

# The movements from earlier guarantee that the top-left zone is non-zero. Make this the initial zone
view_name = widgets_zone[f"zone{1+shift}"][0]
dock = self.docks[view_name]
self.addDockWidget(areas[left_or_right], dock)

# The main logic: apply splittings between different zones and in
# different orders, depending on which zones are present.

# Layouts with two non-zero zones
if present_zones == set([f'zone{1+shift}', f'zone{2+shift}']):
self.make_split(1,2,"horizontal", widgets_zone, shift)
elif present_zones == set([f'zone{1+shift}', f'zone{5+shift}']):
self.make_split(1,5,"vertical", widgets_zone, shift)
elif present_zones == set([f'zone{1+shift}', f'zone{6+shift}']):
self.make_split(1,6,"horizontal", widgets_zone, shift)

# Layouts with three non-zero zones
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}']):
self.make_split(1,2,"horizontal", widgets_zone, shift)
self.make_split(1,5,"vertical", widgets_zone, shift)
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{6+shift}']):
self.make_split(1,2,"horizontal", widgets_zone, shift)
self.make_split(2,6,"vertical", widgets_zone, shift)
elif present_zones == set([f'zone{1+shift}', f'zone{5+shift}', f'zone{6+shift}']):
self.make_split(1,5,"vertical", widgets_zone, shift)
self.make_split(5,6,"horizontal", widgets_zone, shift)

# Layout with four non-zero zones
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}', f'zone{6+shift}']):
self.make_split(1,5,"vertical", widgets_zone, shift)
self.make_split(1,2,"horizontal", widgets_zone, shift)
self.make_split(5,6,"horizontal", widgets_zone, shift)


def make_split(self, zone_index_1, zone_index_2, orientation, widgets_zone, shift):
"""
Splits the zone at `zone_{zone_index_1+shift}` into two zones
(`zone_{zone_index_1+shift}` and `zone_{zone_index_2+shift}`)
with an `orientation` split.
"""
widget_1 = widgets_zone[f"zone{zone_index_1+shift}"][0]
widget_2 = widgets_zone[f"zone{zone_index_2+shift}"][0]
self.splitDockWidget(self.docks[widget_1], self.docks[widget_2], orientations[orientation])

# used by to tell the launcher this is closed
def closeEvent(self, event):
Expand Down
88 changes: 46 additions & 42 deletions spikeinterface_gui/utils_global.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,48 @@
import numpy as np

def get_size_top_row(initial_row, initial_col, is_zone_array, original_zone_array):

if original_zone_array[initial_row][initial_col] == False:
return 0,0

num_rows = is_zone_array[initial_row][initial_col]*1
num_cols = num_rows

num_rows += (not is_zone_array[1][initial_col])*1

if num_rows == 1:
for zone in is_zone_array[0,1+initial_col:]:
if zone == True:
break
num_cols += 1
elif num_rows == 2:
for zone1, zone2 in np.transpose(is_zone_array[:,1+initial_col:]):
if zone1 == True or zone2 == True:
break
num_cols += 1

is_zone_array[initial_row:initial_row+num_rows,initial_col:initial_col+num_cols] = True

return num_rows, num_cols

def get_size_bottom_row(initial_row, initial_col, is_zone_array, original_zone_array):

if original_zone_array[initial_row][initial_col] == False:
return 0,0

num_rows = is_zone_array[initial_row][initial_col]*1
if num_rows == 0:
return 0, 0
num_cols = num_rows

for zone in is_zone_array[1,1+initial_col:]:
if zone == True:
break
else:
num_cols += 1

return num_rows, num_cols
# Functions for the layout

def fill_unnecessary_space(layout_zone, shift):
"""
Used when making layouts. In the zoning algorithm,
certain layouts are equivalent to each other e.g.

zone1 zone2 . .
. . is equivalent to zone5 zone6

and

. zone2 zone1 .
. zone6 is equivalent to zone5 .

This function moves zones left-wards and upwards in a way that preserves
the layouts and ensures that the top-left zone is non-zero.
"""

# Move the right hand column leftwards if the left-hand column is missing
if len(layout_zone[f'zone{1+shift}']) == 0 and len(layout_zone[f'zone{5+shift}']) == 0:
layout_zone[f'zone{1+shift}'] = layout_zone[f'zone{2+shift}']
layout_zone[f'zone{5+shift}'] = layout_zone[f'zone{6+shift}']
layout_zone[f'zone{2+shift}'] = []
layout_zone[f'zone{6+shift}'] = []

# Move the bottom-left zone to the top-left, if the top-left is missing
# These steps reduce the number of layouts we have to consider
if len(layout_zone[f'zone{1+shift}']) == 0:
layout_zone[f'zone{1+shift}'] = layout_zone[f'zone{5+shift}']
layout_zone[f'zone{5+shift}'] = []

return layout_zone


def get_present_zones_in_half_of_layout(layout_zone, shift):
"""
Returns the zones which contain at least one view, for either:
left-hand zones 1,2,5,6 (shift=0)
right-hand zones 3,4,7,8 (shift=2)
"""
zones_in_half = [f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}', f'zone{6+shift}']
half_dict = {key: value for key, value in layout_zone.items() if key in zones_in_half}
is_present = [views is not None and len(views) > 0 for views in half_dict.values()]
present_zones = set(np.array(list(half_dict.keys()))[np.array(is_present)])
return present_zones