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
1 change: 1 addition & 0 deletions PyReconstruct/modules/datatypes/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def get_username() -> str:
"homeview_act": "Home",
"selectall_act": "Ctrl+A",
"deselect_act": "Ctrl+D",
"invertselection_act": "Ctrl+Shift+I",
"edittrace_act": "Ctrl+E",
"mergetraces_act": "Ctrl+M",
"mergeobjects_act": "Ctrl+Shift+M",
Expand Down
32 changes: 31 additions & 1 deletion PyReconstruct/modules/datatypes/section.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,37 @@ def selectAllTraces(self):
self.deselectAllTraces()
for trace in self.tracesAsList():
self.addSelectedTrace(trace)


def invertTraceSelection(self, include_hidden=False):
"""Invert the trace selection: deselect every selected trace and
select every unselected trace.

Only traces visible in the field can become selected: hidden and
group-hidden traces are skipped unless include_hidden is True (the
show-all-traces mode). Locked objects are never selected
(addSelectedTrace refuses them). Selected ztrace points and flags are
left untouched.

(Only meant for GUI use.)

Params:
include_hidden (bool): True if hidden traces may be selected
"""
selected = set(self.selected_traces)
group_hidden = set(self.traces_group_hide)

to_select = []
for trace in self.tracesAsList():
if trace in selected:
continue
if not include_hidden and (trace.hidden or trace in group_hidden):
continue
to_select.append(trace)

self.selected_traces : list[Trace] = []
for trace in to_select:
self.addSelectedTrace(trace)

def hideTraces(self, traces : list = None, hide=True, log_event=True):
"""Hide traces.

Expand Down
1 change: 1 addition & 0 deletions PyReconstruct/modules/gui/dialog/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def getStaticShortcuts(w : QWidget) -> list[QKeySequence]:
"Field Interactions",
("selectall_act", "Select all traces on section"),
("deselect_act", "Deselect all traces on section"),
("invertselection_act", "Invert selected traces on section"),
("edittrace_act", "Edit attributes of selected trace(s)"),
("mergetraces_act", "Merge selected traces"),
("mergeobjects_act", "Merge attributes of selected traces"),
Expand Down
3 changes: 3 additions & 0 deletions PyReconstruct/modules/gui/main/context_menu_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def get_field_menu_list(self):
None,
("selectall_act", "Select all traces", self.series, self.field.selectAllTraces),
("deselect_act", "Deselect traces", self.series, self.field.deselectAllTraces),
("invertselection_act", "Invert selection", self.series, self.field.invertTraceSelection),
None,
("delete_act", "Delete", "Del", self.backspace),
]
Expand Down Expand Up @@ -94,6 +95,8 @@ def get_context_menu_list_obj(self):
None,
("hideobj_act", "Hide", "", self.hideObj),
("unhideobj_act", "Unhide", "", lambda : self.hideObj(False)),
("hideunselectedobj_act", "Hide unselected objects", "", self.hideUnselectedObjects),
("showallobj_act", "Show all objects", "", self.unhideAllObjects),
None,
("removealltags_act", "Remove all tags", "", self.removeAllTags),
None,
Expand Down
8 changes: 8 additions & 0 deletions PyReconstruct/modules/gui/main/field_widget_2_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,14 @@ def selectAllTraces(self):
self.section.selectAllTraces()
self.generateView(generate_image=False)

def invertTraceSelection(self):
"""Invert which traces are selected on the section."""
# disable if trace layer is hidden
if self.hide_trace_layer:
return
self.section.invertTraceSelection(include_hidden=self.show_all_traces)
self.generateView(generate_image=False)

############################################################################
## Interactions only accessible through the field ##########################
############################################################################
Expand Down
45 changes: 45 additions & 0 deletions PyReconstruct/modules/gui/main/field_widget_3_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,51 @@ def hideObj(self, obj_names : list, hide=True):

return True

# update_objects=False so the decorator does NOT run its locked-check on the
# selection (the SELECTED objects are the ones being kept, not modified) and
# does not refresh the wrong rows -- both are handled against the complement
# below.
@object_function(update_objects=False, reload_field=True)
def hideUnselectedObjects(self, obj_names : list):
"""Isolate the selected object(s): hide every OTHER object throughout the
whole series, so the isolation persists as sections change.

Objects in the complement are hidden regardless of their locked state --
locking guards edits and quantification, not visibility. An empty
selection is a no-op (the decorator returns before we get here), so this
can never blank the series.

Params:
obj_names (list): the objects to keep visible (object-list
selection, or the objects owning the field's selected traces)
Returns:
(bool): True if any object was hidden
"""
keep = set(obj_names)
others = [name for name in self.series.data["objects"] if name not in keep]
if not others: # everything is already selected -> nothing to hide
return False

self.series.hideObjects(others, True, self.series_states)
self.table_manager.updateObjects(others)

return True

def unhideAllObjects(self):
"""Show all objects: unhide every object throughout the whole series.

The clear restore for "Hide unselected objects"; undoable series-wide,
like the object hide itself.
"""
all_names = list(self.series.data["objects"].keys())
if not all_names:
return
self.mainwindow.saveAllData()
self.series.hideObjects(all_names, False, self.series_states)
self.table_manager.updateObjects(all_names)
self.mainwindow.seriesModified(True)
self.reload()

@object_function(update_objects=False, reload_field=False)
def addTo3D(self, obj_names : list):
"""Generate a 3D view of an object"""
Expand Down
65 changes: 62 additions & 3 deletions PyReconstruct/modules/gui/table/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
QPalette,
QColor
)
from PySide6.QtCore import Qt
from PySide6.QtCore import Qt, QItemSelection, QItemSelectionModel

from .data_table import DataTable
from .history import HistoryTableWidget
Expand All @@ -36,6 +36,23 @@
TextWidget,
)


def invert_object_rows(row_names : list, selected : set):
"""Return the row indices to select when inverting an object selection.

Every row that is not currently selected becomes selected. Object rows are
freely selectable in the list (no lock restriction), so nothing is excluded
here -- this matches the object list's existing selection behavior.

Params:
row_names (list): object name at each row, in row order
selected (set): names of the currently selected objects
Returns:
(list): the row indices that should end up selected
"""
return [r for r, name in enumerate(row_names) if name not in selected]


class ObjectTableWidget(DataTable):

def __init__(self, series : Series, mainwindow : QWidget, manager, hidden=False):
Expand Down Expand Up @@ -130,6 +147,17 @@ def getCall(col_name, opt_name):
("export_act", "Export...", "", self.export),
]
},
{
"attr_name": "selectionmenu",
"text": "Selection",
"opts":
[
("invertobjselection_act", "Invert selection", "", self.invertSelection),
None,
("hideunselectedobj_act1", "Hide unselected objects", "", self.mainwindow.field.hideUnselectedObjects),
("showallobj_act1", "Show all objects", "", self.mainwindow.field.unhideAllObjects),
]
},
{
"attr_name": "filtermenu",
"text": "Filter",
Expand Down Expand Up @@ -194,8 +222,13 @@ def getCall(col_name, opt_name):
# fill in the menu bar object
populateMenuBar(self, self.menubar, menubar_list)

# create the right-click menu
context_menu_list = self.mainwindow.field.getObjMenu()
# create the right-click menu -- prepend the object-list-only
# "Invert selection" (a table selection op, so it lives here rather
# than in the shared field object menu)
context_menu_list = [
("invertobjselection_act1", "Invert selection", "", self.invertSelection),
None,
] + self.mainwindow.field.getObjMenu()
self.context_menu = QMenu(self)
populateMenu(self, self.context_menu, context_menu_list)

Expand Down Expand Up @@ -575,6 +608,32 @@ def getSelected(self, single=False):
else:
return obj_names

def invertSelection(self):
"""Invert which objects are selected in the list.

Every object shown in the list that is not currently selected becomes
selected, and vice versa. Operates on the rows currently displayed, so
with no active filter this inverts against every object in the series.
"""
row_count = self.table.rowCount()
if not row_count:
return

row_names = [self.table.item(r, 0).text() for r in range(row_count)]
selected = set(self.getSelected())
to_select = invert_object_rows(row_names, selected)

model = self.table.model()
last_col = self.table.columnCount() - 1
new_selection = QItemSelection()
for r in to_select:
new_selection.select(model.index(r, 0), model.index(r, last_col))

self.table.selectionModel().select(
new_selection,
QItemSelectionModel.ClearAndSelect
)

def itemChanged(self, item : QTableWidgetItem):
"""User checked a checkbox."""
# check for curation
Expand Down