From a70a2e1880abed89b169e12fb9a9a9cf23fe0ecf Mon Sep 17 00:00:00 2001 From: chrishalcrow Date: Fri, 26 Sep 2025 06:11:39 +0100 Subject: [PATCH 1/4] refactor greedy layout --- spikeinterface_gui/backend_panel.py | 71 ++++++++++---- spikeinterface_gui/backend_qt.py | 147 ++++++++++------------------ spikeinterface_gui/utils_global.py | 70 ++++++------- 3 files changed, 133 insertions(+), 155 deletions(-) diff --git a/spikeinterface_gui/backend_panel.py b/spikeinterface_gui/backend_panel.py index 32fb954..5f8dd81 100644 --- a/spikeinterface_gui/backend_panel.py +++ b/spikeinterface_gui/backend_panel.py @@ -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() @@ -283,26 +282,60 @@ 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. + + The second box (34,78) is equal to the 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) + + if present_zones == set([f'zone{1+shift}']): + gs[0,0] = layout_zone.get(f'zone{1+shift}') + + 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}') + + 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 diff --git a/spikeinterface_gui/backend_qt.py b/spikeinterface_gui/backend_qt.py index b9f8986..1141dcc 100644 --- a/spikeinterface_gui/backend_qt.py +++ b/spikeinterface_gui/backend_qt.py @@ -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 @@ -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]) @@ -217,103 +217,62 @@ 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 second box (34,78) is equal to the 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 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): + 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): diff --git a/spikeinterface_gui/utils_global.py b/spikeinterface_gui/utils_global.py index 881763b..6fdcc8d 100644 --- a/spikeinterface_gui/utils_global.py +++ b/spikeinterface_gui/utils_global.py @@ -1,44 +1,30 @@ 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 \ No newline at end of file +# Functions for the layout + +def fill_unnecessary_space(layout_zone, shift): + + # First, 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}'] = [] + + # And 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): + """ + Check which zones in layout_zone are + """ + half_dict = {key: value for key, value in layout_zone.items() if key in [f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}', f'zone{6+shift}']} + 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 \ No newline at end of file From 786938f0a5da0b4c6d2bae0cd9db7d90be1e53f7 Mon Sep 17 00:00:00 2001 From: chrishalcrow Date: Sun, 28 Sep 2025 18:57:48 +0100 Subject: [PATCH 2/4] update doc strings --- spikeinterface_gui/backend_qt.py | 5 +++++ spikeinterface_gui/utils_global.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/spikeinterface_gui/backend_qt.py b/spikeinterface_gui/backend_qt.py index 1141dcc..2e7e057 100644 --- a/spikeinterface_gui/backend_qt.py +++ b/spikeinterface_gui/backend_qt.py @@ -270,6 +270,11 @@ def make_half_layout(self, widgets_zone, left_or_right): 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]) diff --git a/spikeinterface_gui/utils_global.py b/spikeinterface_gui/utils_global.py index 6fdcc8d..77e735a 100644 --- a/spikeinterface_gui/utils_global.py +++ b/spikeinterface_gui/utils_global.py @@ -3,6 +3,21 @@ # 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 the top-left zone is non-zero. + """ # First, 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: @@ -22,7 +37,7 @@ def fill_unnecessary_space(layout_zone, shift): def get_present_zones_in_half_of_layout(layout_zone, shift): """ - Check which zones in layout_zone are + Returns the zones which contain at least one view. """ half_dict = {key: value for key, value in layout_zone.items() if key in [f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}', f'zone{6+shift}']} is_present = [views is not None and len(views) > 0 for views in half_dict.values()] From ea2597a68fc98a33378f70a4fa163bd0309a31e2 Mon Sep 17 00:00:00 2001 From: chrishalcrow Date: Tue, 14 Oct 2025 11:10:39 +0100 Subject: [PATCH 3/4] add more docs/comments --- spikeinterface_gui/backend_panel.py | 10 +++++++--- spikeinterface_gui/backend_qt.py | 7 +++++-- spikeinterface_gui/utils_global.py | 13 ++++++++----- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/spikeinterface_gui/backend_panel.py b/spikeinterface_gui/backend_panel.py index 5f8dd81..55e4f1e 100644 --- a/spikeinterface_gui/backend_panel.py +++ b/spikeinterface_gui/backend_panel.py @@ -294,9 +294,10 @@ def make_half_layout(self, gs, layout_zone, left_or_right): 1 2 3 4 5 6 or 7 8 - Then depending on which zones are non-zero, a different layout is generated. + Then depending on which zones are non-zero, a different layout is generated using splits. - The second box (34,78) is equal to the first box (12,56) shifted by 2. We take advantage of this fact. + 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 @@ -304,9 +305,11 @@ def make_half_layout(self, gs, layout_zone, left_or_right): 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}') @@ -330,7 +333,8 @@ def make_half_layout(self, gs, layout_zone, left_or_right): 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}') diff --git a/spikeinterface_gui/backend_qt.py b/spikeinterface_gui/backend_qt.py index 2e7e057..fd924c8 100644 --- a/spikeinterface_gui/backend_qt.py +++ b/spikeinterface_gui/backend_qt.py @@ -226,7 +226,8 @@ def make_half_layout(self, widgets_zone, left_or_right): Then depending on which zones are non-zero, a different layout is generated using splits. - The second box (34,78) is equal to the first box (12,56) shifted by 2. We take advantage of this fact. + 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 @@ -242,7 +243,9 @@ def make_half_layout(self, widgets_zone, left_or_right): dock = self.docks[view_name] self.addDockWidget(areas[left_or_right], dock) - # The main logic: apply splittings depending on which zones are present + # 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) diff --git a/spikeinterface_gui/utils_global.py b/spikeinterface_gui/utils_global.py index 77e735a..48c807a 100644 --- a/spikeinterface_gui/utils_global.py +++ b/spikeinterface_gui/utils_global.py @@ -16,17 +16,17 @@ def fill_unnecessary_space(layout_zone, shift): . zone6 is equivalent to zone5 . This function moves zones left-wards and upwards in a way that preserves - the layouts and ensures the top-left zone is non-zero. + the layouts and ensures that the top-left zone is non-zero. """ - # First, move the right hand column leftwards if the left-hand column is missing + # 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}'] = [] - # And move the bottom-left zone to the top-left, if the top-left is missing + # 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}'] @@ -37,9 +37,12 @@ def fill_unnecessary_space(layout_zone, shift): def get_present_zones_in_half_of_layout(layout_zone, shift): """ - Returns the zones which contain at least one view. + 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) """ - half_dict = {key: value for key, value in layout_zone.items() if key in [f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}', f'zone{6+shift}']} + 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 \ No newline at end of file From 6e8ab5af2ecce192a9844077f5c09a13bd633f18 Mon Sep 17 00:00:00 2001 From: chrishalcrow Date: Tue, 14 Oct 2025 12:23:55 +0100 Subject: [PATCH 4/4] make readme look nice --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 73f6974..583d764 100644 --- a/README.md +++ b/README.md @@ -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.