Skip to content

Views

This page provides the documentation for the views, panel components, and UI widgets.

Views

TiBi.views.BrillouinZonePlotView

Bases: QWidget

A 3D visualization panel for Brillouin Zone using PyQtGraph's OpenGL.

Displays a Brillouin zone as a wireframe with vertices shown as small spheres. The visualization supports rotation and zooming.

Features: - Interactive 3D visualization with mouse rotation and zooming - Colored axes representing the Cartesian coordinate system - BZ visualization with wireframe boundaries - High-symmetry points displayed as colored spheres - Selected high-symmetry points highlighted with a different color

Source code in TiBi/views/bz_plot_view.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class BrillouinZonePlotView(QWidget):
    """
    A 3D visualization panel for Brillouin Zone using PyQtGraph's OpenGL.

    Displays a Brillouin zone as a wireframe with vertices shown as small
    spheres. The visualization supports rotation and zooming.

    Features:
    - Interactive 3D visualization with mouse rotation and zooming
    - Colored axes representing the Cartesian coordinate system
    - BZ visualization with wireframe boundaries
    - High-symmetry points displayed as colored spheres
    - Selected high-symmetry points highlighted with a different color
    """

    def __init__(self):
        super().__init__()
        self.setMinimumHeight(150)

        # Colors
        self.point_color = CF_BLUE
        self.selected_point_color = CF_YELLOW

        # Setup layout
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        # Create 3D plot widget
        self.view = gl.GLViewWidget()
        # Set almost-orthographic projection
        self.view.opts["distance"] = 2000
        self.view.opts["fov"] = 1  # In degrees
        self.view.setBackgroundColor("k")  # Black background

        # Axes
        axis_limit = 10
        axes = [
            np.array([[-axis_limit, 0, 0], [axis_limit, 0, 0]]),
            np.array([[0, -axis_limit, 0], [0, axis_limit, 0]]),
            np.array([[0, 0, -axis_limit], [0, 0, axis_limit]]),
        ]
        for ii, color in enumerate(
            [
                CF_VERMILLION,
                CF_GREEN,
                CF_SKY,
            ]
        ):
            self.view.addItem(
                gl.GLLinePlotItem(
                    pos=axes[ii], color=color, width=5, antialias=True
                )
            )

        layout.addWidget(self.view, stretch=1)

TiBi.views.ComputationView

Bases: QWidget

A multi-tab view for setting up and managing computations.

This view contains the following panels:

  • HoppintPanel: For managing hopping parameters
  • BandsPanel: For managing band structure and Brillouin grid calculations

Attributes:

Name Type Description
hopping_panel HoppingPanel

Panel for editing hopping parameters between states.

bands_panel BandsPanel

Panel for managing band and Brillouin grid calculations.

Source code in TiBi/views/computation_view.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class ComputationView(QWidget):
    """
    A multi-tab view for setting up and managing computations.

    This view contains the following panels:

    - HoppintPanel: For managing hopping parameters
    - BandsPanel: For managing band structure and Brillouin grid calculations

    Attributes
    ----------
    hopping_panel : HoppingPanel
        Panel for editing hopping parameters between states.
    bands_panel : BandsPanel
        Panel for managing band and Brillouin grid calculations.
    """

    def __init__(self):
        super().__init__()

        # Create a layout for the TabView itself
        layout = QVBoxLayout(self)
        layout.setContentsMargins(
            0, 0, 0, 0
        )  # Remove margins to maximize space
        self.hopping_panel = HoppingPanel()
        self.bands_panel = BandsPanel()
        # Create main tab widget
        self.tabs = QTabWidget()
        self.tabs.addTab(self.hopping_panel, "Hopping")
        self.tabs.addTab(self.bands_panel, "Bands")
        self.tabs.setTabPosition(QTabWidget.East)
        self.tabs.setDocumentMode(True)
        # Add the tab widget to the layout
        layout.addWidget(self.tabs)

        # Set the layout for this widget
        self.setLayout(layout)

TiBi.views.MainToolbarView

Bases: QToolBar

Main toolbar view that contains application-wide actions.

This class is a view component that provides a toolbar with common actions such as creating new unit cells, saving/loading projects, etc.

It does not create actions itself, receiving them from an action manager.

Source code in TiBi/views/main_toolbar_view.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class MainToolbarView(QToolBar):
    """
    Main toolbar view that contains application-wide actions.

    This class is a view component that provides a toolbar with common actions
    such as creating new unit cells, saving/loading projects, etc.

    It does not create actions itself, receiving them from an action manager.
    """

    def __init__(self):
        super().__init__("Main Toolbar")

        # Allow the toolbar to be moved by the user
        self.setMovable(True)

    def set_actions(self, action_manager: ActionManager):
        """
        Set actions from the action manager to the toolbar.

        Parameters
        ----------
        action_manager : ActionManager
            ActionManager instance containing all actions to be added.
        """
        # Add File actions
        self.addAction(action_manager.file_actions["new_project"])
        self.addAction(action_manager.file_actions["open_project"])
        self.addAction(action_manager.file_actions["import_project"])
        self.addAction(action_manager.file_actions["save_project"])

        self.addSeparator()

        self.addAction(action_manager.undo_redo_actions["undo"])
        self.addAction(action_manager.undo_redo_actions["redo"])

        self.addSeparator()

        self.addAction(action_manager.unit_cell_actions["wireframe"])
        # Add the grouped spinboxes
        self.addWidget(self._create_uc_spinbox_group())

    def _create_uc_spinbox_group(self):
        """
        Create a grouped widget containing spinboxes for unit cell repetitions.
        """
        group_widget = QWidget()
        layout = QHBoxLayout(group_widget)
        layout.setContentsMargins(0, 0, 0, 0)  # Remove padding to fit nicely
        layout.setSpacing(5)  # Small spacing between spinboxes and labels

        self.n1_spinbox = QSpinBox()
        self.n2_spinbox = QSpinBox()
        self.n3_spinbox = QSpinBox()

        for ii, spinbox in enumerate(
            [self.n1_spinbox, self.n2_spinbox, self.n3_spinbox]
        ):
            spinbox.setRange(1, 10)
            spinbox.setFixedWidth(50)
            spinbox.setToolTip(f"Along v<sub>{ii+1}</sub>")
            spinbox.setStatusTip("Number of unit cells")
            spinbox.setEnabled(False)

        layout.addWidget(QLabel("n<sub>1</sub>:"))
        # layout.addWidget(QLabel("n\u2081:"))
        layout.addWidget(self.n1_spinbox)
        layout.addWidget(QLabel("n<sub>2</sub>:"))
        # layout.addWidget(QLabel("n\u2082:"))
        layout.addWidget(self.n2_spinbox)
        layout.addWidget(QLabel("n<sub>3</sub>:"))
        # layout.addWidget(QLabel("n\u2083:"))
        layout.addWidget(self.n3_spinbox)

        return group_widget

set_actions(action_manager)

Set actions from the action manager to the toolbar.

Parameters:

Name Type Description Default
action_manager ActionManager

ActionManager instance containing all actions to be added.

required
Source code in TiBi/views/main_toolbar_view.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def set_actions(self, action_manager: ActionManager):
    """
    Set actions from the action manager to the toolbar.

    Parameters
    ----------
    action_manager : ActionManager
        ActionManager instance containing all actions to be added.
    """
    # Add File actions
    self.addAction(action_manager.file_actions["new_project"])
    self.addAction(action_manager.file_actions["open_project"])
    self.addAction(action_manager.file_actions["import_project"])
    self.addAction(action_manager.file_actions["save_project"])

    self.addSeparator()

    self.addAction(action_manager.undo_redo_actions["undo"])
    self.addAction(action_manager.undo_redo_actions["redo"])

    self.addSeparator()

    self.addAction(action_manager.unit_cell_actions["wireframe"])
    # Add the grouped spinboxes
    self.addWidget(self._create_uc_spinbox_group())

TiBi.views.MainWindow

Bases: QMainWindow

Main application window that defines the UI layout.

This class is purely a view component that arranges the UI elements and doesn't contain business logic or model manipulation. It creates a four-column layout for organizing the different components of the application, along with menu bar, toolbar, and status bar.

Attributes:

Name Type Description
uc UnitCellView

Unit cell editor view

uc_plot UnitCellPlotView

Unit cell 3D visualization view

bz_plot BrillouinZonePlotView

Brillouin zone 3D visualization view

plot PlotView

2D plot view

computation ComputationView

Multi-tab view used to set up calculations

window_closed Signal

Signals the main app to run the cleanup procedure

Source code in TiBi/views/main_window.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
class MainWindow(QMainWindow):
    """
    Main application window that defines the UI layout.

    This class is purely a view component that arranges the UI elements and
    doesn't contain business logic or model manipulation. It creates a
    four-column layout for organizing the different components of the
    application, along with menu bar, toolbar, and status bar.

    Attributes
    ----------
    uc : UnitCellView
        Unit cell editor view
    uc_plot : UnitCellPlotView
        Unit cell 3D visualization view
    bz_plot : BrillouinZonePlotView
        Brillouin zone 3D visualization view
    plot : PlotView
        2D plot view
    computation : ComputationView
        Multi-tab view used to set up calculations
    window_closed : Signal
        Signals the main app to run the cleanup procedure
    """

    window_closed = Signal()

    def __init__(
        self,
        uc: UnitCellView,
        uc_plot: UnitCellPlotView,
        bz_plot: BrillouinZonePlotView,
        plot: PlotView,
        computation_view: ComputationView,
        menu_bar: MenuBarView,
        toolbar: MainToolbarView,
        status_bar: StatusBarView,
    ):
        super().__init__()
        self.setWindowTitle("TiBi")
        # self.setMinimumSize(1100, 825)

        # Store references to UI components
        self.uc = uc
        self.uc_plot = uc_plot
        self.bz_plot = bz_plot
        self.plot = plot
        self.computation_view = computation_view

        # Set menu bar
        self.setMenuBar(menu_bar)
        # Add toolbar
        self.addToolBar(toolbar)
        # Set status bar
        self.setStatusBar(status_bar)

        # Main Layout
        main_view = QWidget()
        main_layout = QHBoxLayout(main_view)

        # Create three column Layouts and wrap them into Widgets
        # UnitCell geometry and Sites
        unit_cell_layout = QVBoxLayout()
        unit_cell_layout.setContentsMargins(0, 0, 0, 0)
        unit_cell_widget = QWidget()
        unit_cell_widget.setLayout(unit_cell_layout)
        unit_cell_widget.setSizePolicy(
            QSizePolicy.Fixed, QSizePolicy.Expanding
        )
        unit_cell_layout.addWidget(self._frame_widget(self.uc))

        # Computation controls and BZ
        computation_layout = QVBoxLayout()
        computation_layout.setContentsMargins(0, 0, 0, 0)
        computation_layout.setSpacing(7)

        computation_widget = QWidget()
        computation_widget.setLayout(computation_layout)
        computation_widget.setSizePolicy(
            QSizePolicy.Fixed, QSizePolicy.Expanding
        )

        # Set fixed height for computation_view
        fixed_computation_height = self.computation_view.sizeHint().height()
        self.computation_view.setFixedHeight(fixed_computation_height)
        computation_layout.addWidget(self._frame_widget(self.bz_plot))
        computation_layout.addWidget(self._frame_widget(self.computation_view))
        # Add stretch below to allow bz_plot to stretch
        computation_layout.setStretch(0, 1)  # bz_plot expands
        computation_layout.setStretch(1, 0)  # computation_view stays fixed

        # UC 3D plot and results plots
        plots_splitter = QSplitter(Qt.Vertical)
        plots_splitter.addWidget(
            self._frame_widget(self.uc_plot)
        )  # Top 3D plot
        plots_splitter.addWidget(
            self._frame_widget(self.plot)
        )  # Bottom 2D plot

        # Set initial size ratio
        plots_splitter.setSizes([1, 1])
        # Prevent the panels from collapsing
        plots_splitter.setCollapsible(0, False)
        plots_splitter.setCollapsible(1, False)

        main_layout.addWidget(unit_cell_widget)
        main_layout.addWidget(computation_widget)
        main_layout.addWidget(plots_splitter)
        # Set as central widget
        self.setCentralWidget(main_view)

    def closeEvent(self, event):
        # Override the parent class closeEvent to emit a signal on closing.
        self.window_closed.emit()
        super().closeEvent(event)

    def _frame_widget(self, widget: QWidget) -> QFrame:
        """
        Enclose a widget in a frame.

        Used to make the layout look more structured.
        """
        frame = QFrame()
        frame.setFrameShape(QFrame.Box)
        frame.setLineWidth(1)
        layout = QVBoxLayout()
        layout.setContentsMargins(5, 5, 5, 5)
        layout.addWidget(widget, stretch=1)
        frame.setLayout(layout)
        return frame

TiBi.views.MenuBarView

Bases: QMenuBar

Menu bar view that provides access to application features.

This class is a view component that organizes application actions into menus, providing a standard way to access all functionality.

It does not create actions itself, receiving them from an action manager.

Methods:

Name Description
set_actions

Set actions from the action manager to the appropriate menus.

Source code in TiBi/views/menu_bar_view.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class MenuBarView(QMenuBar):
    """
    Menu bar view that provides access to application features.

    This class is a view component that organizes application actions
    into menus, providing a standard way to access all functionality.

    It does not create actions itself, receiving them from an action manager.

    Methods
    -------
    set_actions(action_manager: ActionManager)
        Set actions from the action manager to the appropriate menus.
    """

    def __init__(self):
        super().__init__()

        # Create empty menus
        self.file_menu = QMenu("&File", self)
        self.edit_menu = QMenu("&Edit", self)
        self.view_menu = QMenu("&View", self)
        self.computation_menu = QMenu("&Computation", self)
        self.help_menu = QMenu("&Help", self)

        # Add menus to the menu bar
        self.addMenu(self.file_menu)
        # self.addMenu(self.edit_menu)
        # self.addMenu(self.view_menu)
        # self.addMenu(self.computation_menu)
        # self.addMenu(self.help_menu)

    def set_actions(self, action_manager):
        """
        Set actions from the action manager to the appropriate menus.

        Parameters
        ----------
        action_manager : ActionManager
            ActionManager instance containing all actions to be added.
        """
        # Populate File menu
        self.file_menu.addAction(action_manager.file_actions["new_project"])
        self.file_menu.addAction(action_manager.file_actions["open_project"])
        self.file_menu.addAction(action_manager.file_actions["import_project"])

        self.file_menu.addSeparator()
        self.file_menu.addAction(action_manager.file_actions["save_project"])
        self.file_menu.addAction(
            action_manager.file_actions["save_project_as"]
        )

set_actions(action_manager)

Set actions from the action manager to the appropriate menus.

Parameters:

Name Type Description Default
action_manager ActionManager

ActionManager instance containing all actions to be added.

required
Source code in TiBi/views/menu_bar_view.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def set_actions(self, action_manager):
    """
    Set actions from the action manager to the appropriate menus.

    Parameters
    ----------
    action_manager : ActionManager
        ActionManager instance containing all actions to be added.
    """
    # Populate File menu
    self.file_menu.addAction(action_manager.file_actions["new_project"])
    self.file_menu.addAction(action_manager.file_actions["open_project"])
    self.file_menu.addAction(action_manager.file_actions["import_project"])

    self.file_menu.addSeparator()
    self.file_menu.addAction(action_manager.file_actions["save_project"])
    self.file_menu.addAction(
        action_manager.file_actions["save_project_as"]
    )

TiBi.views.PlotView

Bases: QWidget

Widget for displaying 2D plots.

This widget creates a matplotlib figure embedded in a Qt widget to display data as 2D plots. It includes navigation controls for zooming, panning, and saving the plot.

Source code in TiBi/views/plot_view.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class PlotView(QWidget):
    """
    Widget for displaying 2D plots.

    This widget creates a matplotlib figure embedded in a Qt widget to display
    data as 2D plots. It includes navigation controls for zooming, panning,
    and saving the plot.
    """

    def __init__(self):
        super().__init__()
        self.setMinimumHeight(150)

        # Setup layout
        layout = QVBoxLayout(self)
        layout.setContentsMargins(1, 1, 1, 1)

        # Matplotlib Figure
        self.figure = mpl_fig.Figure(figsize=(5, 4), dpi=100)
        self.canvas = FigureCanvas(self.figure)
        self.toolbar = NavigationToolbar(self.canvas, self)
        self.toolbar.setIconSize(QSize(20, 20))

        # Add widgets to layout
        layout.addWidget(self.toolbar)
        layout.addWidget(self.canvas, stretch=1)

        # Initialize plot
        self.ax = self.figure.add_subplot(111)
        self.ax.grid(True)

        # Initial draw
        self.canvas.draw()

TiBi.views.ProgressDialog

Bases: QDialog

A modal that displays a progress bar and a cancel button.

Attributes:

Name Type Description
cancel_requested Signal

Signal emitted when the cancel button is clicked.

Methods:

Name Description
update_progress

Updates the progress bar with the given value.

Source code in TiBi/views/progress_dialog.py
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class ProgressDialog(QDialog):
    """
    A modal that displays a progress bar and a cancel button.

    Attributes
    ----------
    cancel_requested : Signal
        Signal emitted when the cancel button is clicked.

    Methods
    -------
    update_progress(value: int)
        Updates the progress bar with the given value.
    """

    cancel_requested = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Working...")
        self.setModal(True)

        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)

        self.cancel_button = QPushButton("Cancel")
        self.cancel_button.clicked.connect(self.cancel_requested.emit)

        layout = QVBoxLayout()
        layout.addWidget(self.progress_bar)
        layout.addWidget(self.cancel_button)
        self.setLayout(layout)

    def update_progress(self, value: int):
        """
        Update the progress bar with the given value.
        """
        self.progress_bar.setValue(value)

update_progress(value)

Update the progress bar with the given value.

Source code in TiBi/views/progress_dialog.py
38
39
40
41
42
def update_progress(self, value: int):
    """
    Update the progress bar with the given value.
    """
    self.progress_bar.setValue(value)

TiBi.views.StatusBarView

Bases: QStatusBar

Status bar view that displays application status information.

This class is a view component that shows messages to the user.

Methods:

Name Description
update_status

Update the status label.

Source code in TiBi/views/status_bar_view.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class StatusBarView(QStatusBar):
    """
    Status bar view that displays application status information.

    This class is a view component that shows messages to the user.

    Methods
    -------
    update_status(text: str)
        Update the status label.
    """

    def __init__(self):
        super().__init__()

        # Create a permanent status label
        self.status_label = QLabel("Ready")
        self.addPermanentWidget(self.status_label)

    def update_status(self, text: str):
        """
        Update the status label.

        Parameters
        ----------
        text : str
            New status text to display
        """
        self.status_label.setText(text)

update_status(text)

Update the status label.

Parameters:

Name Type Description Default
text str

New status text to display

required
Source code in TiBi/views/status_bar_view.py
23
24
25
26
27
28
29
30
31
32
def update_status(self, text: str):
    """
    Update the status label.

    Parameters
    ----------
    text : str
        New status text to display
    """
    self.status_label.setText(text)

TiBi.views.UnitCellPlotView

Bases: QWidget

A 3D visualization panel for UnitCell using PyQtGraph's OpenGL support.

Displays a unit cell as a wireframe parallelepiped with sites as spheres. The visualization supports rotation and zooming.

Features: - Interactive 3D visualization with mouse rotation and zooming - Colored axes representing the Cartesian coordinate system - Unit cell visualization with wireframe parallelepiped - Sites displayed as colored spheres at their fractional positions - Selected sites highlighted with an increased size

Source code in TiBi/views/uc_plot_view.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class UnitCellPlotView(QWidget):
    """
    A 3D visualization panel for `UnitCell` using PyQtGraph's OpenGL support.

    Displays a unit cell as a wireframe parallelepiped with sites as spheres.
    The visualization supports rotation and zooming.

    Features:
    - Interactive 3D visualization with mouse rotation and zooming
    - Colored axes representing the Cartesian coordinate system
    - Unit cell visualization with wireframe parallelepiped
    - Sites displayed as colored spheres at their fractional positions
    - Selected sites highlighted with an increased size
    """

    def __init__(self):
        super().__init__()
        self.setMinimumHeight(150)

        # Setup layout
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        # Create 3D plot widget
        self.view = gl.GLViewWidget()
        # Set almost-orthographic projection
        self.view.opts["distance"] = 2000
        self.view.opts["fov"] = 1  # In degrees

        self.view.setBackgroundColor("k")  # Black background

        # Axes
        axis_limit = 10
        axes = [
            np.array([[-axis_limit, 0, 0], [axis_limit, 0, 0]]),
            np.array([[0, -axis_limit, 0], [0, axis_limit, 0]]),
            np.array([[0, 0, -axis_limit], [0, 0, axis_limit]]),
        ]
        for ii, color in enumerate(
            [
                CF_VERMILLION,
                CF_GREEN,
                CF_SKY,
            ]
        ):
            self.view.addItem(
                gl.GLLinePlotItem(
                    pos=axes[ii], color=color, width=5, antialias=True
                )
            )

        layout.addWidget(self.view)

TiBi.views.UnitCellView

Bases: QWidget

Main UI component for managing unit cells, sites, and states.

This widget combines a tree view of the unit cell hierarchy with dynamically swappable panels for editing properties of the selected tree node. It handles the data models and coordinates interactions between the tree view and detail panels.

The UI consists of several main parts:

  1. Tree view panel showing the hierarchy of unit cells, sites, and states
  2. Button panel with actions for creating, deleting, and modifying items
  3. Form panels that change depending on what is selected in the tree
  4. Dimensionality controls for setting periodic boundary conditions

Attributes:

Name Type Description
unit_cell_panel UnitCellPanel

Panel for editing UnitCell properties.

site_panel SitePanel

Panel for editing Site properties.

tree_view_panel TreeViewPanel

Panel displaying the tree view of UnitCells, Sites, and States.

Source code in TiBi/views/uc_view.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class UnitCellView(QWidget):
    """
    Main UI component for managing unit cells, sites, and states.

    This widget combines a tree view of the unit cell hierarchy with
    dynamically swappable panels for editing properties of the selected
    tree node. It handles the data models and coordinates interactions
    between the tree view and detail panels.

    The UI consists of several main parts:

    1. Tree view panel showing the hierarchy of unit cells, sites, and states
    2. Button panel with actions for creating, deleting, and modifying items
    3. Form panels that change depending on what is selected in the tree
    4. Dimensionality controls for setting periodic boundary conditions

    Attributes
    ----------
    unit_cell_panel : UnitCellPanel
        Panel for editing `UnitCell` properties.
    site_panel : SitePanel
        Panel for editing `Site` properties.
    tree_view_panel : TreeViewPanel
        Panel displaying the tree view of `UnitCell`s, `Site`s, and `State`s.
    """

    def __init__(self):
        super().__init__()
        # Main layout
        layout = QVBoxLayout(self)
        layout.setContentsMargins(1, 1, 1, 1)

        # Initialize UI panels
        self.unit_cell_panel = UnitCellPanel()
        self.site_panel = SitePanel()
        self.tree_view_panel = TreeViewPanel()

        # Info labels
        self.uc_info_label = QLabel("Add/Select a Unit Cell")
        self.uc_info_label.setAlignment(Qt.AlignCenter)

        self.site_info_label = QLabel("Add/Select a Site")
        self.site_info_label.setAlignment(Qt.AlignCenter)

        # Panel stacks
        self.uc_stack = QStackedWidget()
        self.uc_stack.addWidget(self.uc_info_label)
        self.uc_stack.addWidget(self.unit_cell_panel)

        self.site_stack = QStackedWidget()
        self.site_stack.addWidget(self.site_info_label)
        self.site_stack.addWidget(self.site_panel)

        # Create the interface
        # Top panel contains the tree view
        top_panel = QVBoxLayout()
        top_panel.addWidget(self.tree_view_panel)

        # Bottom panel contains the unit cell/state editable fields
        bottom_panel = QVBoxLayout()
        bottom_panel.addWidget(self.uc_stack)
        bottom_panel.addWidget(divider_line())
        bottom_panel.addWidget(self.site_stack)

        layout.setSpacing(0)

        layout.addLayout(top_panel)
        layout.addWidget(divider_line())
        layout.addLayout(bottom_panel)
        layout.setStretch(0, 1)
        layout.setStretch(1, 0)
        layout.setStretch(2, 0)

Panel Components

TiBi.views.panels.BandsPanel

Bases: QWidget

A panel for managing band structure calculations and projections.

This panel allows users to define a path in the Brillouin zone, compute band structures, and project states onto various basis states. The panel also includes options for configuring the Brillouin zone grid and DOS calculations.

Source code in TiBi/views/panels/bands_panel.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
class BandsPanel(QWidget):
    """
    A panel for managing band structure calculations and projections.

    This panel allows users to define a path in the Brillouin zone, compute
    band structures, and project states onto various basis states.
    The panel also includes options for configuring
    the Brillouin zone grid and DOS calculations.
    """

    def __init__(self):
        super().__init__()

        # Main Layout
        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        self.bands_grid = QGridLayout()
        self.proj_layout = QVBoxLayout()
        self.dos_layout = QVBoxLayout()
        layout.addLayout(self.bands_grid, stretch=1)
        layout.addWidget(divider_line())
        layout.addLayout(self.proj_layout)
        layout.addWidget(divider_line())
        layout.addLayout(self.dos_layout, stretch=1)

        # Bands Section
        self.bands_grid.setContentsMargins(10, 5, 15, 5)
        path_label = QLabel("Band Calculations")
        path_label.setProperty("style", "bold")
        path_label.setAlignment(
            Qt.AlignCenter
        )  # This centers the text within the label
        self.bands_grid.addWidget(path_label, 0, 0, 1, 8)

        # Icons
        left_arrow_icon = QIcon()
        left_arrow_icon.addFile(
            str(get_resource_path("assets/icons/down_arrow.svg")),
            mode=QIcon.Mode.Normal,
        )
        left_arrow_icon.addFile(
            str(get_resource_path("assets/icons/down_arrow_disabled.svg")),
            mode=QIcon.Mode.Disabled,
        )

        right_arrow_icon = QIcon()
        right_arrow_icon.addFile(
            str(get_resource_path("assets/icons/up_arrow.svg")),
            mode=QIcon.Mode.Normal,
        )
        right_arrow_icon.addFile(
            str(get_resource_path("assets/icons/up_arrow_disabled.svg")),
            mode=QIcon.Mode.Disabled,
        )

        # Gamma point controls (Γ - origin of reciprocal space)
        self.add_gamma_btn = QPushButton("Γ")
        self.add_gamma_btn.setToolTip("Gamma Point")
        self.add_gamma_btn.setStatusTip("Add the Gamma Point to the path.")
        self.add_gamma_btn.setFixedSize(30, 30)
        self.bands_grid.addWidget(self.add_gamma_btn, 2, 0)
        # Vertex selection controls
        self.prev_vertex_btn = QPushButton()
        self.prev_vertex_btn.setToolTip("Previous")
        self.prev_vertex_btn.setStatusTip("Select the previous Vertex.")
        self.prev_vertex_btn.setIcon(left_arrow_icon)
        self.prev_vertex_btn.setIconSize(self.prev_vertex_btn.sizeHint())

        self.next_vertex_btn = QPushButton()
        self.next_vertex_btn.setToolTip("Next")
        self.next_vertex_btn.setStatusTip("Select the next Vertex.")
        self.next_vertex_btn.setIcon(right_arrow_icon)
        self.next_vertex_btn.setIconSize(self.next_vertex_btn.sizeHint())

        self.add_vertex_btn = QPushButton("V")
        self.add_vertex_btn.setToolTip("Vertex")
        self.add_vertex_btn.setStatusTip(
            "Add the selected Vertex to the path."
        )
        self.prev_vertex_btn.setFixedSize(30, 30)
        self.next_vertex_btn.setFixedSize(30, 30)
        self.add_vertex_btn.setFixedSize(30, 30)

        self.bands_grid.addWidget(self.next_vertex_btn, 1, 1)
        self.bands_grid.addWidget(self.add_vertex_btn, 2, 1)
        self.bands_grid.addWidget(self.prev_vertex_btn, 3, 1)

        # Edge midpoint selection controls
        self.prev_edge_btn = QPushButton()
        self.prev_edge_btn.setToolTip("Previous")
        self.prev_edge_btn.setStatusTip("Select the previous Edge.")
        self.prev_edge_btn.setIcon(left_arrow_icon)
        self.prev_edge_btn.setIconSize(self.prev_edge_btn.sizeHint())

        self.next_edge_btn = QPushButton()
        self.next_edge_btn.setToolTip("Next")
        self.next_edge_btn.setStatusTip("Select the next Edge.")
        self.next_edge_btn.setIcon(right_arrow_icon)
        self.next_edge_btn.setIconSize(self.next_edge_btn.sizeHint())

        self.add_edge_btn = QPushButton("E")
        self.add_edge_btn.setToolTip("Edge")
        self.add_edge_btn.setStatusTip("Add the selected Edge to the path.")
        self.prev_edge_btn.setFixedSize(30, 30)
        self.next_edge_btn.setFixedSize(30, 30)
        self.add_edge_btn.setFixedSize(30, 30)

        self.bands_grid.addWidget(self.next_edge_btn, 1, 2)
        self.bands_grid.addWidget(self.add_edge_btn, 2, 2)
        self.bands_grid.addWidget(self.prev_edge_btn, 3, 2)

        # Face center selection controls
        self.prev_face_btn = QPushButton()
        self.prev_face_btn.setToolTip("Previous")
        self.prev_face_btn.setStatusTip("Select the previous Face.")
        self.prev_face_btn.setIcon(left_arrow_icon)
        self.prev_face_btn.setIconSize(self.prev_face_btn.sizeHint())

        self.next_face_btn = QPushButton()
        self.next_face_btn.setToolTip("Next")
        self.next_face_btn.setStatusTip("Select the next Face.")
        self.next_face_btn.setIcon(right_arrow_icon)
        self.next_face_btn.setIconSize(self.next_face_btn.sizeHint())

        self.add_face_btn = QPushButton("F")
        self.add_face_btn.setToolTip("Face")
        self.add_face_btn.setStatusTip("Add the selected Face to the path.")
        self.prev_face_btn.setFixedSize(30, 30)
        self.next_face_btn.setFixedSize(30, 30)
        self.add_face_btn.setFixedSize(30, 30)

        self.bands_grid.addWidget(self.next_face_btn, 1, 3)
        self.bands_grid.addWidget(self.add_face_btn, 2, 3)
        self.bands_grid.addWidget(self.prev_face_btn, 3, 3)

        # Path controls
        self.remove_last_btn = QPushButton()
        remove_last = QIcon()
        remove_last.addFile(
            str(get_resource_path("assets/icons/remove_last.svg")),
            mode=QIcon.Mode.Normal,
        )
        remove_last.addFile(
            str(get_resource_path("assets/icons/remove_last_disabled.svg")),
            mode=QIcon.Mode.Disabled,
        )
        self.remove_last_btn.setIcon(remove_last)
        self.remove_last_btn.setFixedSize(30, 30)
        self.remove_last_btn.setIconSize(self.remove_last_btn.sizeHint())
        self.remove_last_btn.setToolTip("Remove Last")
        self.remove_last_btn.setStatusTip(
            "Remove the last added point from the Brillouin Zone path."
        )

        self.clear_path_btn = QPushButton()
        clear_path_icon = QIcon()
        clear_path_icon.addFile(
            str(get_resource_path("assets/icons/trash_hopping.svg")),
            mode=QIcon.Mode.Normal,
        )
        clear_path_icon.addFile(
            str(get_resource_path("assets/icons/trash_hopping_disabled.svg")),
            mode=QIcon.Mode.Disabled,
        )
        self.clear_path_btn.setIcon(clear_path_icon)
        self.clear_path_btn.setFixedSize(30, 30)
        self.clear_path_btn.setIconSize(self.clear_path_btn.sizeHint())
        self.clear_path_btn.setToolTip("Clear")
        self.clear_path_btn.setStatusTip("Clear the Brillouin Zone path.")

        kpoints_layout = QHBoxLayout()
        self.n_points_spinbox = QSpinBox()
        self.n_points_spinbox.setRange(1, 10000)
        self.n_points_spinbox.setValue(100)
        self.n_points_spinbox.setButtonSymbols(QSpinBox.NoButtons)
        set_spinbox_digit_width(self.n_points_spinbox, 5)

        self.compute_bands_btn = QPushButton()
        compute_icon = QIcon()
        compute_icon.addFile(
            str(get_resource_path("assets/icons/computer.svg")),
            mode=QIcon.Mode.Normal,
        )
        compute_icon.addFile(
            str(get_resource_path("assets/icons/computer_disabled.svg")),
            mode=QIcon.Mode.Disabled,
        )
        self.compute_bands_btn.setIcon(compute_icon)
        self.compute_bands_btn.setFixedSize(30, 30)
        self.compute_bands_btn.setIconSize(self.compute_bands_btn.sizeHint())
        self.compute_bands_btn.setToolTip("Compute")
        self.compute_bands_btn.setStatusTip("Compute the Bands.")

        self.bands_grid.addWidget(self.remove_last_btn, 1, 5)
        self.remove_last_btn.setEnabled(
            False
        )  # Disabled until path has points

        self.bands_grid.addWidget(self.clear_path_btn, 1, 6)
        self.clear_path_btn.setEnabled(False)  # Disabled until path has points

        self.bands_grid.addWidget(self.compute_bands_btn, 1, 7)
        self.compute_bands_btn.setEnabled(
            False
        )  # Disabled until path has at least two points

        kpoints_layout.addWidget(QLabel("Points:"))
        kpoints_layout.addWidget(self.n_points_spinbox)
        self.bands_grid.addLayout(kpoints_layout, 2, 5, 1, 3)

        self.bands_grid.setVerticalSpacing(5)
        self.bands_grid.setHorizontalSpacing(5)

        # Initially disable all selection buttons
        btns = [
            self.add_gamma_btn,
            self.prev_vertex_btn,
            self.next_vertex_btn,
            self.add_vertex_btn,
            self.prev_edge_btn,
            self.next_edge_btn,
            self.add_edge_btn,
            self.prev_face_btn,
            self.next_face_btn,
            self.add_face_btn,
        ]
        for btn in btns:
            btn.setEnabled(False)

        # Group buttons by type for easier controller access
        self.vertex_btns = [
            self.prev_vertex_btn,
            self.next_vertex_btn,
            self.add_vertex_btn,
        ]
        self.edge_btns = [
            self.prev_edge_btn,
            self.next_edge_btn,
            self.add_edge_btn,
        ]
        self.face_btns = [
            self.prev_face_btn,
            self.next_face_btn,
            self.add_face_btn,
        ]

        for row in range(4):  # Rows 0 to 4 in bands_grid
            self.bands_grid.setRowMinimumHeight(row, 25)

        # Approximate output size label
        self.approximate_band_size = QLabel("Approximate output size: 0 kB")
        self.bands_grid.addWidget(self.approximate_band_size, 4, 0, 1, 8)

        # Projection panel
        projection_label = QLabel("State Projection")
        projection_label.setProperty("style", "bold")
        projection_label.setAlignment(Qt.AlignCenter)
        projection_tools = QHBoxLayout()

        self.proj_layout.setContentsMargins(10, 5, 15, 5)

        self.proj_layout.addWidget(projection_label)
        self.proj_layout.addLayout(projection_tools)

        self.proj_combo = CheckableComboBox()
        self.project_btn = QPushButton("Project")
        self.select_all_btn = QPushButton("All")
        self.select_all_btn.setToolTip("Select All")
        self.select_all_btn.setStatusTip("Select all States for Projection.")
        self.clear_all_btn = QPushButton("None")
        self.clear_all_btn.setToolTip("Clear Selection")
        self.clear_all_btn.setStatusTip("Deselect all States.")
        self.select_all_btn.setEnabled(False)
        self.clear_all_btn.setEnabled(False)

        proj_left = QVBoxLayout()
        proj_right = QVBoxLayout()
        proj_buttons = QHBoxLayout()

        projection_tools.addLayout(proj_left, stretch=3)
        projection_tools.addLayout(proj_right, stretch=1)

        proj_left.addWidget(self.proj_combo)
        proj_left.addLayout(proj_buttons)

        proj_buttons.addWidget(self.select_all_btn)
        proj_buttons.addWidget(self.clear_all_btn)

        self.bands_radio = QRadioButton("Bands")
        self.dos_radio = QRadioButton("DOS")

        self.radio_group = QButtonGroup(self)
        self.radio_group.addButton(self.bands_radio, id=0)
        self.radio_group.addButton(self.dos_radio, id=1)
        self.bands_radio.setChecked(True)

        proj_right.addWidget(self.bands_radio)
        proj_right.addWidget(self.dos_radio)

        # DOS Section
        self.bz_grid = QGridLayout()
        self.dos_visualization_grid = QGridLayout()
        self.dos_layout.addLayout(self.bz_grid)
        self.dos_layout.addWidget(divider_line())
        self.dos_layout.addLayout(self.dos_visualization_grid)
        self.dos_layout.setContentsMargins(10, 5, 15, 5)

        bz_grid_label = QLabel("Brillouin Zone Grid")
        bz_grid_label.setProperty("style", "bold")
        bz_grid_label.setAlignment(Qt.AlignCenter)
        self.bz_grid.addWidget(bz_grid_label, 0, 0, 1, 4)

        dos_presentation_label = QLabel("DOS Visualization")
        dos_presentation_label.setProperty("style", "bold")
        dos_presentation_label.setAlignment(Qt.AlignCenter)
        self.dos_visualization_grid.addWidget(
            dos_presentation_label, 0, 0, 1, 4
        )

        # Grid points controls
        self.v1_points_spinbox = QSpinBox()
        self.v2_points_spinbox = QSpinBox()
        self.v3_points_spinbox = QSpinBox()
        self.num_bins_spinbox = EnterKeyIntSpinBox()
        self.broadening_spinbox = EnterKeySpinBox()

        for b in [
            self.v1_points_spinbox,
            self.v2_points_spinbox,
            self.v3_points_spinbox,
        ]:
            b.setRange(2, 200)
            b.setValue(30)
            set_spinbox_digit_width(b, 3)
            b.setButtonSymbols(QSpinBox.NoButtons)
            b.setEnabled(False)

        self.num_bins_spinbox.setRange(2, 1000)
        self.num_bins_spinbox.setValue(20)
        self.num_bins_spinbox.setButtonSymbols(QSpinBox.NoButtons)
        set_spinbox_digit_width(self.num_bins_spinbox, 5)

        self.broadening_spinbox.setDecimals(3)
        self.broadening_spinbox.setRange(0.001, 10.0)
        self.broadening_spinbox.setValue(0.001)
        self.broadening_spinbox.setButtonSymbols(QSpinBox.NoButtons)
        set_spinbox_digit_width(self.broadening_spinbox, 5)

        self.bz_grid.addWidget(QLabel("v<sub>1</sub> points:"), 1, 0)
        self.bz_grid.addWidget(QLabel("v<sub>2</sub> points:"), 2, 0)
        self.bz_grid.addWidget(QLabel("v<sub>3</sub> points:"), 3, 0)

        self.bz_grid.addWidget(self.v1_points_spinbox, 1, 1)
        self.bz_grid.addWidget(self.v2_points_spinbox, 2, 1)
        self.bz_grid.addWidget(self.v3_points_spinbox, 3, 1)

        # Approximate output size label
        self.approximate_BZ_grid_size = QLabel("Approximate output size: 0 kB")
        self.bz_grid.addWidget(self.approximate_BZ_grid_size, 4, 0, 1, 4)

        self.dos_visualization_grid.addWidget(QLabel("Bin number:"), 1, 0)
        self.dos_visualization_grid.addWidget(QLabel("Broadening:"), 2, 0)

        self.dos_visualization_grid.addWidget(self.num_bins_spinbox, 1, 1)
        self.dos_visualization_grid.addWidget(self.broadening_spinbox, 2, 1)

        self.compute_grid_btn = QPushButton()
        self.compute_grid_btn.setIcon(compute_icon)
        self.compute_grid_btn.setFixedSize(30, 30)
        self.compute_grid_btn.setIconSize(self.compute_grid_btn.sizeHint())
        self.compute_grid_btn.setToolTip("Compute")
        self.compute_grid_btn.setStatusTip("Compute the Brillouin Zone grid.")
        self.compute_grid_btn.setEnabled(False)

        # Grid type choice
        self.MP_radio = QRadioButton("Monkhorst-Pack")
        self.Gamma_radio = QRadioButton("Γ-centered")
        self.grid_choice_group = QButtonGroup(self)
        self.grid_choice_group.addButton(self.MP_radio, id=0)
        self.grid_choice_group.addButton(self.Gamma_radio, id=1)
        self.MP_radio.setChecked(True)

        # DOS presentation choice
        self.histogram_radio = QRadioButton("Histogram")
        self.lorentzian_radio = QRadioButton("Lorentzian")
        self.presentation_choice_group = QButtonGroup(self)
        self.presentation_choice_group.addButton(self.histogram_radio, id=0)
        self.presentation_choice_group.addButton(self.lorentzian_radio, id=1)
        self.histogram_radio.setChecked(True)

        self.bz_grid.addWidget(self.MP_radio, 1, 3)
        self.bz_grid.addWidget(self.Gamma_radio, 2, 3)
        self.bz_grid.addWidget(self.compute_grid_btn, 3, 3)

        self.dos_visualization_grid.addWidget(self.histogram_radio, 1, 3)
        self.dos_visualization_grid.addWidget(self.lorentzian_radio, 2, 3)

TiBi.views.panels.hopping_panel.HoppingMatrix

Bases: QWidget

A grid of buttons representing possible hopping connections between states.

Each button in the grid represents a possible hopping between two states. The rows (columns) represent the destination (source) states. Buttons are colored differently based on whether a hopping exists or not, and whether the hopping is Hermitian.

Source code in TiBi/views/panels/hopping_panel.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class HoppingMatrix(QWidget):
    """
    A grid of buttons representing possible hopping connections between states.

    Each button in the grid represents a possible hopping between two states.
    The rows (columns) represent the destination (source) states.
    Buttons are colored differently based on whether a hopping exists or not,
    and whether the hopping is Hermitian.
    """

    def __init__(self):
        super().__init__()

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        # Scrollable Area for Matrix
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)

        # Content widget for grid
        self.content_widget = QWidget()
        self.grid_layout = QGridLayout(self.content_widget)
        self.grid_layout.setSpacing(3)

        self.scroll_area.setWidget(self.content_widget)
        self.scroll_area.setFrameStyle(QFrame.NoFrame)

        title_label = QLabel("Coupling Matrix")
        title_label.setProperty("style", "bold")
        title_label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        title_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        layout.addWidget(title_label)
        layout.addWidget(self.scroll_area)

TiBi.views.panels.HoppingPanel

Bases: QWidget

View for editing hopping parameters between quantum states.

This widget combines two main components:

  1. A matrix grid where each button represents a possible hopping connection between two states [rows (columns) are destination (target) states]
  2. A table for editing specific hopping parameters when a connection is selected in the matrix

The view uses a stacked widget approach to show different panels based on the current selection state (no unit cell, no states, or states selected).

Attributes:

Name Type Description
matrix_panel HoppingMatrix

The panel displaying the hopping matrix as a grid of buttons.

table_panel HoppingTable

The panel displaying the hopping parameters in a table format.

Source code in TiBi/views/panels/hopping_panel.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
class HoppingPanel(QWidget):
    """
    View for editing hopping parameters between quantum states.

    This widget combines two main components:

    1. A matrix grid where each button represents a possible hopping connection
       between two states [rows (columns) are destination (target) states]
    2. A table for editing specific hopping parameters when a connection
       is selected in the matrix

    The view uses a stacked widget approach to show different panels based on
    the current selection state (no unit cell, no states, or states selected).

    Attributes
    ----------
    matrix_panel : HoppingMatrix
        The panel displaying the hopping matrix as a grid of buttons.
    table_panel : HoppingTable
        The panel displaying the hopping parameters in a table format.
    """

    def __init__(self):
        super().__init__()

        # Initialize the panels
        self.matrix_panel = HoppingMatrix()
        self.table_panel = HoppingTable()

        # Main layout
        layout = QVBoxLayout(self)
        layout.setContentsMargins(1, 1, 1, 1)

        # Components
        self.info_label = QLabel("Select a Unit Cell with States")
        self.info_label.setAlignment(Qt.AlignCenter)

        self.table_info_label = QLabel("Select a matrix element to edit")
        self.table_info_label.setAlignment(Qt.AlignCenter)

        # Main Panel
        self.panel = QWidget()
        panel_layout = QVBoxLayout(self.panel)

        # A stack that hides the table if no state pair is selected
        self.table_stack = QStackedWidget()
        self.table_stack.addWidget(self.table_info_label)
        self.table_stack.addWidget(self.table_panel)

        panel_layout.addWidget(self.matrix_panel, stretch=3)
        panel_layout.addWidget(self.table_stack, stretch=2)

        # A stack that hides the main panel if no unit cell is selected or
        # the selected unit cell has no states
        self.panel_stack = QStackedWidget()
        self.panel_stack.addWidget(self.info_label)
        self.panel_stack.addWidget(self.panel)
        layout.addWidget(self.panel_stack)

TiBi.views.panels.hopping_panel.HoppingTable

Bases: QWidget

A table containing hoppings between the selected states.

Each row of the table describes a hopping and contains five columns. The first three columns provide the displacements from the origin site to the destination site in terms of the basis vectors. The last two columns are the real and imaginary parts of the hopping term, respectively.

Source code in TiBi/views/panels/hopping_panel.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
class HoppingTable(QWidget):
    """
    A table containing hoppings between the selected states.

    Each row of the table describes a hopping and contains five columns.
    The first three columns provide the displacements from the origin site
    to the destination site in terms of the basis vectors. The last two
    columns are the real and imaginary parts of the hopping term, respectively.
    """

    def __init__(self):
        super().__init__()

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        self.table_title = QLabel("")
        self.table_title.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)

        # Table manipulation buttons on the right
        table_buttons_layout = QVBoxLayout()

        self.add_row_btn = QPushButton()
        self.add_row_btn.setIcon(
            QIcon(str(get_resource_path("assets/icons/plus_hopping.svg")))
        )
        self.add_row_btn.setFixedSize(30, 30)
        self.add_row_btn.setIconSize(self.add_row_btn.sizeHint())
        self.add_row_btn.setToolTip("New Hopping")
        self.add_row_btn.setStatusTip("Add a new hopping integral.")

        self.remove_row_btn = QPushButton()
        self.remove_row_btn.setIcon(
            QIcon(str(get_resource_path("assets/icons/trash_hopping.svg")))
        )
        self.remove_row_btn.setFixedSize(30, 30)
        self.remove_row_btn.setIconSize(self.remove_row_btn.sizeHint())
        self.remove_row_btn.setToolTip("Delete Hopping")
        self.remove_row_btn.setStatusTip(
            "Delete the selected hopping integral."
        )

        self.save_btn = QPushButton()
        self.save_btn.setIcon(
            QIcon(str(get_resource_path("assets/icons/save_hopping.svg")))
        )
        self.save_btn.setFixedSize(30, 30)
        self.save_btn.setIconSize(self.save_btn.sizeHint())
        self.save_btn.setToolTip("Save Hoppings")
        self.save_btn.setStatusTip("Save the Hopping table.")

        table_buttons_layout.addWidget(self.add_row_btn)
        table_buttons_layout.addWidget(self.remove_row_btn)
        table_buttons_layout.addWidget(self.save_btn)

        # Table for hopping details
        self.hopping_table = QTableWidget(
            0, 5
        )  # 5 columns: d1, d2, d3, Re(amplitude), Im(amplitude)

        headers = ["d₁", "d₂", "d₃", "Re(t)", "Im(t)"]
        tooltips = [
            "Displacement along v₁",
            "Displacement along v₂",
            "Displacement along v₃",
            "Real part",
            "Imaginary part",
        ]
        statustips = [
            "Number of v₁'s from the origin site to the destination site",
            "Number of v₂'s from the origin site to the destination site",
            "Number of v₃'s from the origin site to the destination site",
            "Real part of hopping amplitude",
            "Imaginary part of hopping amplitude",
        ]

        # Assign headers with tooltips
        for i, (label, tip, status) in enumerate(
            zip(headers, tooltips, statustips)
        ):
            item = QTableWidgetItem(label)
            item.setToolTip(tip)
            item.setStatusTip(status)
            self.hopping_table.setHorizontalHeaderItem(i, item)

        # Get font metrics for the spinbox's current font
        font_metrics = QFontMetrics(self.hopping_table.font())

        # Add some padding for margins, borders, and spin buttons
        padding = 10  # Adjust as needed

        self.hopping_table.setColumnWidth(
            0, font_metrics.horizontalAdvance("0" * 3) + padding
        )  # d₁
        self.hopping_table.setColumnWidth(
            1, font_metrics.horizontalAdvance("0" * 3) + padding
        )  # d₂
        self.hopping_table.setColumnWidth(
            2, font_metrics.horizontalAdvance("0" * 3) + padding
        )  # d₃
        self.hopping_table.setColumnWidth(
            3, font_metrics.horizontalAdvance("0" * 6) + padding
        )  # Real amplitude part
        self.hopping_table.setColumnWidth(
            4, font_metrics.horizontalAdvance("0" * 6) + padding
        )  # Imaginary amplitude part

        # Table and buttons layout
        control_layout = QHBoxLayout()
        control_layout.addWidget(self.hopping_table)
        control_layout.addLayout(table_buttons_layout)

        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_area.setWidget(self.table_title)
        scroll_area.setFrameStyle(QFrame.NoFrame)
        content_height = self.table_title.sizeHint().height()
        scroll_area.setMaximumHeight(content_height + 20)

        layout.addWidget(scroll_area)
        layout.addLayout(control_layout)

TiBi.views.panels.SitePanel

Bases: QWidget

Form panel for editing site properties.

This panel provides a form interface for editing a site's properties:

  • Radius for the site marker
  • Color for the site marker
  • Fractional coordinates (c1, c2, c3) within the unit cell
Source code in TiBi/views/panels/site_panel.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class SitePanel(QWidget):
    """
    Form panel for editing site properties.

    This panel provides a form interface for editing a site's properties:

    - Radius for the site marker
    - Color for the site marker
    - Fractional coordinates (c1, c2, c3) within the unit cell
    """

    def __init__(self):
        super().__init__()

        # Main layout
        layout = QVBoxLayout(self)

        panel_header = QLabel("Site Parameters")
        panel_header.setProperty("style", "bold")
        panel_header.setAlignment(Qt.AlignCenter)

        header = QLabel("Site Coordinates")
        header.setAlignment(Qt.AlignCenter)

        # Coordinate and radius fields
        self.R = EnterKeySpinBox()
        self.c1 = EnterKeySpinBox()
        self.c2 = EnterKeySpinBox()
        self.c3 = EnterKeySpinBox()

        for c in [self.R, self.c1, self.c2, self.c3]:
            c.setRange(0.0, 1.0)
            c.setDecimals(3)
            c.setButtonSymbols(EnterKeySpinBox.NoButtons)
            set_spinbox_digit_width(c, 5)

        # Color picker button
        self.color_picker_btn = QPushButton()
        self.color_picker_btn.setFixedWidth(25)

        appearance_layout = QHBoxLayout()
        appearance_layout.addWidget(QLabel("Radius:"))
        appearance_layout.addWidget(self.R)
        appearance_layout.addWidget(QLabel("Color:"))
        appearance_layout.addWidget(self.color_picker_btn)

        # Create a grid layout with labels on top and spin boxes below
        c1_label = QLabel("c<sub>1</sub>")
        c1_label.setAlignment(Qt.AlignCenter)
        c2_label = QLabel("c<sub>2</sub>")
        c2_label.setAlignment(Qt.AlignCenter)
        c3_label = QLabel("c<sub>3</sub>")
        c3_label.setAlignment(Qt.AlignCenter)

        grid_layout = QGridLayout()
        grid_layout.addWidget(c1_label, 1, 0)
        grid_layout.addWidget(c2_label, 1, 1)
        grid_layout.addWidget(c3_label, 1, 2)

        grid_layout.addWidget(self.c1, 2, 0)
        grid_layout.addWidget(self.c2, 2, 1)
        grid_layout.addWidget(self.c3, 2, 2)
        grid_layout.setVerticalSpacing(2)

        layout.addWidget(panel_header)
        layout.addLayout(appearance_layout)
        layout.addWidget(header)
        layout.addLayout(grid_layout)

TiBi.views.panels.tree_view_panel.TreeDelegate

Bases: QStyledItemDelegate

A custom item delegate for the SystemTree widget.

This delegate requires the user to commit changes to tree item names by pressing "Enter". Clicking away/defocusing resets the tree item name to its pre-edit form. The purpose is to handle the Qt default behavior, where defocusing keeps the new display name in the tree but does not send an updated signal so that the data can be updated internally. Additionally, the delegate draws rectangular "Delete" and "Add" buttons next to the item names.

Attributes:

Name Type Description
name_edit_finished Signal(object)

Emitted when the user finishes editing an item name.

new_unit_cell_requested Signal

Emitted when the user clicks the "Add Unit Cell" button.

new_site_requested Signal

Emitted when the user adds a new Site.

new_state_requested Signal

Emitted when the user adds a new State.

delete_requested Signal

Emitted when the user clicks the "Delete" button next to an item.

Source code in TiBi/views/panels/tree_view_panel.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
class TreeDelegate(QStyledItemDelegate):
    """
    A custom item delegate for the `SystemTree` widget.

    This delegate requires the user to commit changes to tree item names by
    pressing "Enter". Clicking away/defocusing resets the tree item name to
    its pre-edit form. The purpose is to handle the Qt default behavior,
    where defocusing keeps the new display name in the tree but does not send
    an updated signal so that the data can be updated internally.
    Additionally, the delegate draws rectangular "Delete" and "Add" buttons
    next to the item names.

    Attributes
    ----------
    name_edit_finished : Signal(object)
        Emitted when the user finishes editing an item name.
    new_unit_cell_requested : Signal
        Emitted when the user clicks the "Add Unit Cell" button.
    new_site_requested : Signal
        Emitted when the user adds a new `Site`.
    new_state_requested : Signal
        Emitted when the user adds a new `State`.
    delete_requested : Signal
        Emitted when the user clicks the "Delete" button next to an item.
    """

    name_edit_finished = Signal(object)  # Emits QModelIndex
    new_unit_cell_requested = Signal()
    new_site_requested = Signal()
    new_state_requested = Signal()
    delete_requested = Signal()

    def __init__(self, parent=None):
        super().__init__(parent)
        self._editing_index = None
        self.delete_renderer = QSvgRenderer(
            str(get_resource_path("assets/icons/trash.svg"))
        )
        self.add_renderer = QSvgRenderer(
            str(get_resource_path("assets/icons/plus.svg"))
        )

    def _button_rects(self, option, index):
        """Regions defining the item buttons."""
        size = 16
        margin = 4
        right = option.rect.right() - margin

        buttons = {}
        buttons["delete"] = QRect(
            right - size, option.rect.center().y() - size // 2, size, size
        )
        if self._get_item_level(index) < 2:
            buttons["add"] = QRect(
                right - 2 * size - margin,
                option.rect.center().y() - size // 2,
                size,
                size,
            )
        return buttons

    def _get_item_level(self, index):
        """
        Determine the tree level of the item (0=UnitCell, 1=Site, 2=State)
        """
        level = 0
        current = index
        while current.parent().isValid():
            level += 1
            current = current.parent()
        return level

    def createEditor(self, parent, option, index):
        editor = super().createEditor(parent, option, index)
        editor.setProperty(
            "originalText", index.data()
        )  # Save the original name
        self._editing_index = index
        return editor

    def paint(self, painter, option, index):
        if index.data(Qt.UserRole) == "ADD_UNIT_CELL":
            # Custom paint for "Add Unit Cell" item
            painter.save()
            # Get the full width of the viewport (the whole row)
            tree = self.parent()
            full_rect = QRect(
                0,
                option.rect.y(),
                tree.viewport().width(),
                option.rect.height(),
            )

            # Draw background across the full width
            painter.fillRect(
                full_rect,
                QColor(*hex_to_rgb(THEME_SETTINGS["PRIMARY_HEX"])),
            )

            # Text formatting
            painter.setPen(QColor(*hex_to_rgb(THEME_SETTINGS["ON_PRIMARY"])))
            font = painter.font()
            painter.setFont(font)

            # Adjust text inside the original rect (with padding)
            text_rect = option.rect.adjusted(8, 0, -8, 0)
            painter.drawText(text_rect, Qt.AlignVCenter, "+ Add Unit Cell")

            painter.restore()

        else:
            super().paint(painter, option, index)
            # Only show the delete icon for selected items
            if option.state & QStyle.State_Selected:
                rects = self._button_rects(option, index)
                self.delete_renderer.render(painter, rects["delete"])
                if "add" in rects:
                    # painter.drawPixmap(rects["add"], self.add_icon)
                    self.add_renderer.render(painter, rects["add"])

    def editorEvent(self, event, model, option, index):
        # Handle special "Add Unit Cell" item
        if index.data(Qt.UserRole) == "ADD_UNIT_CELL":
            if event.type() == QEvent.MouseButtonRelease:
                if option.rect.contains(event.pos()):
                    self.new_unit_cell_requested.emit()
                    return True
            return False

        rects = self._button_rects(option, index)

        if event.type() == QEvent.MouseButtonPress:
            # Store whether the item was selected before this click
            self._was_selected_before_click = (
                self.parent().selectionModel().isSelected(index)
            )
            return super().editorEvent(event, model, option, index)

        elif event.type() == QEvent.MouseButtonRelease:
            if rects["delete"].contains(event.pos()):
                # Only trigger delete if the item was already selected
                if self._was_selected_before_click:
                    self.delete_requested.emit()
                    return True
                # If it wasn't selected before, the click selects the item
                return False
            elif ("add" in rects) and rects["add"].contains(event.pos()):
                # Only trigger delete if the item was already selected
                if self._was_selected_before_click:
                    if self._get_item_level(index) == 0:
                        self.new_site_requested.emit()
                    elif self._get_item_level(index) == 1:
                        self.new_state_requested.emit()
                    return True
                return False
        return super().editorEvent(event, model, option, index)

    def eventFilter(self, editor, event):
        if event.type() == QEvent.FocusOut:
            # Restore original text on focus loss (cancel edit)
            editor.blockSignals(True)
            editor.setText(editor.property("originalText"))
            editor.blockSignals(False)
            self.closeEditor.emit(editor, QStyledItemDelegate.RevertModelCache)
            return True

        if event.type() == QEvent.KeyPress and event.key() in (
            Qt.Key_Return,
            Qt.Key_Enter,
        ):
            # User manually pressed Enter or Return — accept the edit
            self.commitData.emit(editor)
            self.closeEditor.emit(editor, QStyledItemDelegate.NoHint)
            if self._editing_index is not None:
                self.name_edit_finished.emit(self._editing_index)
                self._editing_index = None
            return True

        return super().eventFilter(editor, event)

TiBi.views.panels.TreeViewPanel

Bases: QWidget

Tree view panel for displaying and selecting the unit cell hierarchy.

This panel displays a hierarchical tree showing UnitCells, their Sites, and the States at each Site. It handles selection events and emits signals when different types of nodes are selected, allowing other components to respond appropriately.

The tree has three levels:

  1. UnitCells
  2. Sites within a UnitCell
  3. States within a Site

Features:

  • Hierarchical display of unit cells, sites, and states
  • Single selection mode for focused editing
  • Double-click to edit names directly in the tree
  • Keyboard shortcuts for deletion (Del and Backspace)
  • Signal emission on deletion requests

Attributes:

Name Type Description
delegate TreeDelegate

Custom tree delegate for handling item editing and button actions.

tree_view SystemTree

The tree view widget displaying the unit cell hierarchy.

name_edit_finished Signal(object)

Emitted when the user finishes editing an item name. Re-emitting signal from the TreeDelegate.`

new_unit_cell_requested Signal

Emitted when the user clicks the "Add Unit Cell" button. Re-emitting signal from the TreeDelegate.`

new_site_requested Signal

Emitted when the user adds a new Site. Re-emitting signal from the TreeDelegate.`

new_state_requested Signal

Emitted when the user adds a new State. Re-emitting signal from the TreeDelegate.`

delete_requested Signal

Emitted when the user presses the Delete/Backspace key. Also acts as a re-emitting signal for deletion requests from the TreeDelegate.

Source code in TiBi/views/panels/tree_view_panel.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
class TreeViewPanel(QWidget):
    """
    Tree view panel for displaying and selecting the unit cell hierarchy.

    This panel displays a hierarchical tree showing `UnitCell`s, their `Site`s,
    and the `State`s at each `Site`.
    It handles selection events and emits signals when different types
    of nodes are selected, allowing other components to respond appropriately.

    The tree has three levels:

    1. `UnitCell`s
    2. `Sites` within a `UnitCell`
    3. `State`s within a `Site`

    Features:

    - Hierarchical display of unit cells, sites, and states
    - Single selection mode for focused editing
    - Double-click to edit names directly in the tree
    - Keyboard shortcuts for deletion (Del and Backspace)
    - Signal emission on deletion requests

    Attributes
    ----------
    delegate : TreeDelegate
        Custom tree delegate for handling item editing and button actions.
    tree_view : SystemTree
        The tree view widget displaying the unit cell hierarchy.
    name_edit_finished : Signal(object)
        Emitted when the user finishes editing an item name.
        Re-emitting signal from the `TreeDelegate`.`
    new_unit_cell_requested : Signal
        Emitted when the user clicks the "Add Unit Cell" button.
        Re-emitting signal from the `TreeDelegate`.`
    new_site_requested : Signal
        Emitted when the user adds a new `Site`.
        Re-emitting signal from the `TreeDelegate`.`
    new_state_requested : Signal
        Emitted when the user adds a new `State`.
        Re-emitting signal from the `TreeDelegate`.`
    delete_requested : Signal
        Emitted when the user presses the Delete/Backspace key.
        Also acts as a re-emitting signal for deletion requests
        from the `TreeDelegate`.
    """

    # Define signals
    name_edit_finished = Signal(object)  # Emits QModelIndex
    new_unit_cell_requested = Signal()
    new_site_requested = Signal()
    new_state_requested = Signal()
    delete_requested = Signal()

    def __init__(self):
        super().__init__()

        # Create and configure tree view
        self.tree_view = SystemTree()
        self.setMaximumWidth(220)

        # Set the delegate to save the data only on Enter-press
        self.delegate = TreeDelegate(self.tree_view)
        self.tree_view.setItemDelegate(self.delegate)

        # Layout
        layout = QVBoxLayout(self)
        layout.addWidget(self.tree_view)

        # Set up Delete shortcut
        self.delete_shortcut = QShortcut(QKeySequence("Del"), self.tree_view)
        self.delete_shortcut.activated.connect(
            lambda: self.delete_requested.emit()
        )

        # Add Backspace as an alternative shortcut
        self.backspace_shortcut = QShortcut(
            QKeySequence("Backspace"), self.tree_view
        )
        self.backspace_shortcut.activated.connect(
            lambda: self.delete_requested.emit()
        )

        # Relay delegate signals
        self.delegate.delete_requested.connect(
            lambda: QTimer.singleShot(0, lambda: self.delete_requested.emit())
        )
        self.delegate.new_unit_cell_requested.connect(
            lambda: QTimer.singleShot(
                0, lambda: self.new_unit_cell_requested.emit()
            )
        )
        self.delegate.new_site_requested.connect(
            lambda: QTimer.singleShot(
                0, lambda: self.new_site_requested.emit()
            )
        )
        self.delegate.new_state_requested.connect(
            lambda: QTimer.singleShot(
                0, lambda: self.new_state_requested.emit()
            )
        )
        self.delegate.name_edit_finished.connect(
            lambda x: QTimer.singleShot(
                0, lambda x=x: self.name_edit_finished.emit(x)
            )
        )

TiBi.views.panels.UnitCellPanel

Bases: QWidget

Form panel for editing unit cell properties.

This panel provides a form interface for editing a unit cell's properties:

  • System dimensionality
  • Three basis vectors (v1, v2, v3) with x, y, z components

Methods:

Name Description
set_basis_vectors

Set the basis vectors in the UI.

Source code in TiBi/views/panels/unit_cell_panel.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
class UnitCellPanel(QWidget):
    """
    Form panel for editing unit cell properties.

    This panel provides a form interface for editing a unit cell's properties:

    - System dimensionality
    - Three basis vectors (v1, v2, v3) with x, y, z components

    Methods
    -------
    set_basis_vectors(v1: BasisVector, v2: BasisVector, v3: BasisVector)
        Set the basis vectors in the UI.
    """

    def __init__(self):
        super().__init__()

        # Main layout
        layout = QVBoxLayout(self)

        # Layout for basis vectors
        grid_layout = QGridLayout()

        panel_header = QLabel("Unit Cell Parameters")
        panel_header.setProperty("style", "bold")
        panel_header.setAlignment(Qt.AlignCenter)

        dimensionality_header = QLabel("Dimensionality")
        dimensionality_header.setAlignment(Qt.AlignCenter)

        basis_header = QLabel("Basis Vectors")
        basis_header.setAlignment(Qt.AlignCenter)

        # Dimensionality radio buttons
        self.radio0D = QRadioButton("0")
        self.radio1D = QRadioButton("1")
        self.radio2D = QRadioButton("2")
        self.radio3D = QRadioButton("3")

        self.radio_group = QButtonGroup(self)
        self.radio_group.addButton(self.radio0D, id=0)
        self.radio_group.addButton(self.radio1D, id=1)
        self.radio_group.addButton(self.radio2D, id=2)
        self.radio_group.addButton(self.radio3D, id=3)

        radio_layout = QHBoxLayout()
        radio_layout.addWidget(self.radio0D)
        radio_layout.addWidget(self.radio1D)
        radio_layout.addWidget(self.radio2D)
        radio_layout.addWidget(self.radio3D)

        # Add Site and Reduce UC (LLL algorithm) buttons
        self.reduce_btn = QPushButton()
        self.reduce_btn.setFixedSize(20, 20)
        self.reduce_btn.setToolTip("Reduce Unit Cell")
        self.reduce_btn.setStatusTip(
            "Reduce Unit Cell basis using Lenstra–Lenstra–Lovász algorithm"
        )

        # Assemble the panel
        layout.addWidget(panel_header)
        layout.addWidget(dimensionality_header)
        layout.addLayout(radio_layout)
        layout.addLayout(grid_layout)

        # Populate the basis vector grid
        grid_layout.addWidget(basis_header, 0, 0, 1, 4)
        grid_layout.addWidget(self.reduce_btn, 1, 0)
        self.reduce_btn.setIcon(
            QIcon(str(get_resource_path("assets/icons/grid.svg")))
        )

        # Function to create a row with (x, y, z) input fields
        def create_vector_column(n):
            x = EnterKeySpinBox()
            y = EnterKeySpinBox()
            z = EnterKeySpinBox()

            for coord in [x, y, z]:
                coord.setButtonSymbols(EnterKeySpinBox.NoButtons)
                coord.setRange(-1e308, 1e308)
                set_spinbox_digit_width(coord, 5)
                coord.setDecimals(3)

            # Vector components are stacked vertically
            grid_layout.addWidget(x, 2, n)
            grid_layout.addWidget(y, 3, n)
            grid_layout.addWidget(z, 4, n)
            return (x, y, z)

        # Create vector input columns
        self.v1 = create_vector_column(1)
        self.v2 = create_vector_column(2)
        self.v3 = create_vector_column(3)

        # Vector labels go above the coordinate columns
        v1_label = QLabel("v<sub>1</sub>")
        v1_label.setAlignment(Qt.AlignCenter)
        v2_label = QLabel("v<sub>2</sub>")
        v2_label.setAlignment(Qt.AlignCenter)
        v3_label = QLabel("v<sub>3</sub>")
        v3_label.setAlignment(Qt.AlignCenter)

        grid_layout.addWidget(v1_label, 1, 1)
        grid_layout.addWidget(v2_label, 1, 2)
        grid_layout.addWidget(v3_label, 1, 3)

        # Create a coordinate label column to the left of coordinate columns
        for ii, (text, color) in enumerate(
            zip(["x", "y", "z"], [CF_VERMILLION, CF_GREEN, CF_SKY]), start=1
        ):
            label = QLabel(text)
            label.setAlignment(Qt.AlignCenter)
            c = (
                int(color[0] * 255),
                int(color[1] * 255),
                int(color[2] * 255),
                int(color[3]),
            )  # Color in 0-255 component range
            label.setStyleSheet(f"color: rgba({c[0]},{c[1]},{c[2]},{c[3]});")
            grid_layout.addWidget(label, 1 + ii, 0)

        grid_layout.setVerticalSpacing(2)
        grid_layout.setColumnStretch(0, 0)
        grid_layout.setColumnStretch(1, 1)
        grid_layout.setColumnStretch(2, 1)
        grid_layout.setColumnStretch(3, 1)

    def set_basis_vectors(
        self, v1: BasisVector, v2: BasisVector, v3: BasisVector
    ) -> None:
        """
        Set the basis vectors in the UI.

        Parameters
        ----------
        v1 : BasisVector
            Basis vector 1.
        v2 : BasisVector
            Basis vector 2.
        v3 : BasisVector
            Basis vector 3.
        """
        for ii, coord in enumerate("xyz"):
            self.v1[ii].setValue(getattr(v1, coord))
            self.v2[ii].setValue(getattr(v2, coord))
            self.v3[ii].setValue(getattr(v3, coord))

set_basis_vectors(v1, v2, v3)

Set the basis vectors in the UI.

Parameters:

Name Type Description Default
v1 BasisVector

Basis vector 1.

required
v2 BasisVector

Basis vector 2.

required
v3 BasisVector

Basis vector 3.

required
Source code in TiBi/views/panels/unit_cell_panel.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def set_basis_vectors(
    self, v1: BasisVector, v2: BasisVector, v3: BasisVector
) -> None:
    """
    Set the basis vectors in the UI.

    Parameters
    ----------
    v1 : BasisVector
        Basis vector 1.
    v2 : BasisVector
        Basis vector 2.
    v3 : BasisVector
        Basis vector 3.
    """
    for ii, coord in enumerate("xyz"):
        self.v1[ii].setValue(getattr(v1, coord))
        self.v2[ii].setValue(getattr(v2, coord))
        self.v3[ii].setValue(getattr(v3, coord))

UI Widgets

TiBi.views.widgets.CheckableComboBox

Bases: QComboBox

Drop-down box that supports multiple selections.

Selected items are denoted by ticks.

Attributes:

Name Type Description
selection_changed Signal(object)

Emitted when the selection changes. Even if multiple items are selected/deselected, the signal is emitted once. The signal carries a list of indices of the selected items.

Methods:

Name Description
checked_items

Get the indices of the selected items.

clear_selection

Deselect all items.

refresh_combo

Reset the menu with a new list of items.

select_all

Select all items.

Source code in TiBi/views/widgets/checkable_combo_box.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class CheckableComboBox(QComboBox):
    """
    Drop-down box that supports multiple selections.

    Selected items are denoted by ticks.

    Attributes
    ----------
    selection_changed : Signal(object)
        Emitted when the selection changes. Even if multiple items
        are selected/deselected, the signal is emitted once. The signal
        carries a list of indices of the selected items.

    Methods
    -------
    checked_items
        Get the indices of the selected items.
    clear_selection
        Deselect all items.
    refresh_combo(list[str])
        Reset the menu with a new list of items.
    select_all
        Select all items.
    """

    selection_changed = Signal(object)

    def __init__(self):
        super().__init__()
        # Create model
        self.combo_model = QStandardItemModel()
        # Set model to view
        self.setModel(self.combo_model)
        self.combo_model.itemChanged.connect(
            lambda _: self.selection_changed.emit(self.checked_items())
        )

    def refresh_combo(self, items: list[str]):
        """
        Reset the combo box with a new list of items.

        Parameters
        ----------
        items : list[str]
            New list of items to be added to the menu.
        """
        self.combo_model.clear()
        for idx in range(len(items)):
            # for text in items:
            text = items[idx]
            item = QStandardItem(text)
            item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
            item.setData(Qt.Checked, Qt.CheckStateRole)
            item.setData(idx, Qt.UserRole)
            self.combo_model.appendRow(item)
        self.selection_changed.emit(self.checked_items())

    def checked_items(self):
        """
        Get the indices of the selected items.

        Returns
        -------
        result : list[int]
            List of indices of selected items
        """
        result = []
        for ii in range(self.combo_model.rowCount()):
            item = self.combo_model.item(ii)
            if item.checkState() == Qt.Checked:
                result.append(item.data(Qt.UserRole))
        return result

    def clear_selection(self):
        """
        Deselect all items.
        """
        self.combo_model.blockSignals(True)
        for ii in range(self.combo_model.rowCount()):
            item = self.combo_model.item(ii)
            item.setData(Qt.Unchecked, Qt.CheckStateRole)
        self.combo_model.blockSignals(False)
        self.selection_changed.emit(self.checked_items())

    def select_all(self):
        """
        Select all items.
        """
        self.combo_model.blockSignals(True)
        for ii in range(self.combo_model.rowCount()):
            item = self.combo_model.item(ii)
            item.setData(Qt.Checked, Qt.CheckStateRole)
        self.combo_model.blockSignals(False)
        self.selection_changed.emit(self.checked_items())

checked_items()

Get the indices of the selected items.

Returns:

Name Type Description
result list[int]

List of indices of selected items

Source code in TiBi/views/widgets/checkable_combo_box.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def checked_items(self):
    """
    Get the indices of the selected items.

    Returns
    -------
    result : list[int]
        List of indices of selected items
    """
    result = []
    for ii in range(self.combo_model.rowCount()):
        item = self.combo_model.item(ii)
        if item.checkState() == Qt.Checked:
            result.append(item.data(Qt.UserRole))
    return result

clear_selection()

Deselect all items.

Source code in TiBi/views/widgets/checkable_combo_box.py
79
80
81
82
83
84
85
86
87
88
def clear_selection(self):
    """
    Deselect all items.
    """
    self.combo_model.blockSignals(True)
    for ii in range(self.combo_model.rowCount()):
        item = self.combo_model.item(ii)
        item.setData(Qt.Unchecked, Qt.CheckStateRole)
    self.combo_model.blockSignals(False)
    self.selection_changed.emit(self.checked_items())

refresh_combo(items)

Reset the combo box with a new list of items.

Parameters:

Name Type Description Default
items list[str]

New list of items to be added to the menu.

required
Source code in TiBi/views/widgets/checkable_combo_box.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def refresh_combo(self, items: list[str]):
    """
    Reset the combo box with a new list of items.

    Parameters
    ----------
    items : list[str]
        New list of items to be added to the menu.
    """
    self.combo_model.clear()
    for idx in range(len(items)):
        # for text in items:
        text = items[idx]
        item = QStandardItem(text)
        item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
        item.setData(Qt.Checked, Qt.CheckStateRole)
        item.setData(idx, Qt.UserRole)
        self.combo_model.appendRow(item)
    self.selection_changed.emit(self.checked_items())

select_all()

Select all items.

Source code in TiBi/views/widgets/checkable_combo_box.py
90
91
92
93
94
95
96
97
98
99
def select_all(self):
    """
    Select all items.
    """
    self.combo_model.blockSignals(True)
    for ii in range(self.combo_model.rowCount()):
        item = self.combo_model.item(ii)
        item.setData(Qt.Checked, Qt.CheckStateRole)
    self.combo_model.blockSignals(False)
    self.selection_changed.emit(self.checked_items())

TiBi.views.widgets.EnterKeyIntSpinBox

Bases: QSpinBox

Custom QSpinBox that emits a signal when the Enter key is pressed.

On defocus, the value is reset to the original value and no signal is emitted.

Attributes:

Name Type Description
editingConfirmed Signal

Signal emitted when the Enter key is pressed and the value has changed.

Source code in TiBi/views/widgets/enter_key_int_spin_box.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class EnterKeyIntSpinBox(QSpinBox):
    """
    Custom `QSpinBox` that emits a signal when the Enter key is pressed.

    On defocus, the value is reset to the original value and no signal
    is emitted.

    Attributes
    ----------
    editingConfirmed : Signal
        Signal emitted when the Enter key is pressed and the value has changed.
    """

    editingConfirmed = Signal()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._original_value = self.value()

    def focusInEvent(self, event):
        self._original_value = self.value()
        super().focusInEvent(event)

    def focusOutEvent(self, event):
        # Revert to original value on focus out
        self.blockSignals(True)
        self.setValue(self._original_value)
        self.blockSignals(False)
        super().focusOutEvent(event)

    def keyPressEvent(self, event):
        if event.key() in (Qt.Key_Enter, Qt.Key_Return):
            # Emit signal only if the value has changed
            if not np.isclose(self.value(), self._original_value):
                self._original_value = self.value()
                self.editingConfirmed.emit()
        elif event.key() == Qt.Key_Escape:
            self.blockSignals(True)
            self.setValue(self._original_value)
            self.blockSignals(False)
        else:
            super().keyPressEvent(event)

TiBi.views.widgets.EnterKeySpinBox

Bases: QDoubleSpinBox

Custom QDoubleSpinBox that emits a signal when the Enter key is pressed.

On defocus, the value is reset to the original value and no signal is emitted.

Attributes:

Name Type Description
editingConfirmed Signal

Signal emitted when the Enter key is pressed and the value has changed.

Source code in TiBi/views/widgets/enter_key_spin_box.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class EnterKeySpinBox(QDoubleSpinBox):
    """
    Custom `QDoubleSpinBox` that emits a signal when the Enter key is pressed.

    On defocus, the value is reset to the original value and no signal
    is emitted.

    Attributes
    ----------
    editingConfirmed : Signal
        Signal emitted when the Enter key is pressed and the value has changed.
    """

    editingConfirmed = Signal()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._original_value = self.value()

    def focusInEvent(self, event):
        self._original_value = self.value()
        super().focusInEvent(event)

    def focusOutEvent(self, event):
        # Revert to original value on focus out
        self.blockSignals(True)
        self.setValue(self._original_value)
        self.blockSignals(False)
        super().focusOutEvent(event)

    def keyPressEvent(self, event):
        if event.key() in (Qt.Key_Enter, Qt.Key_Return):
            # Emit signal only if the value has changed
            if not np.isclose(self.value(), self._original_value):
                self._original_value = self.value()
                self.editingConfirmed.emit()
        elif event.key() == Qt.Key_Escape:
            self.blockSignals(True)
            self.setValue(self._original_value)
            self.blockSignals(False)
        else:
            super().keyPressEvent(event)

TiBi.views.widgets.SystemTree

Bases: QTreeView

Custom QTreeViewtreefor displaying UnitCells, Sites, and States.

This tree view is designed to show a hierarchical structure of UnitCells, Sites, and States. It allows for easy navigation and selection of these elements. The tree is built using a QStandardItemModel, and each item in the tree is represented by a QStandardItem. The tree supports single selection mode and can be edited by double-clicking on an item. The tree view emits a signal when the selection changes, providing information about the selected UnitCell, Site, and State.

Attributes:

Name Type Description
tree_model QStandardItemModel

The model used to populate the tree view.

root_node QStandardItem

The root item of the tree model, representing the tree's top level.

tree_selection_changed Signal

Emitted when the selection in the tree changes. The signal carries a dictionary with the selected UnitCell, Site, and State IDs.

Methods:

Name Description
add_tree_item

Add and select a tree item without rebuilding the entire tree.

find_item_by_id

Find a tree item by its ID.

refresh_tree

Rebuilds the entire tree from the current data model.

remove_tree_item

Remove an item from the tree.

Source code in TiBi/views/widgets/system_tree.py
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
class SystemTree(QTreeView):
    """
    Custom `QTreeViewtree`for displaying `UnitCell`s, `Site`s, and `State`s.

    This tree view is designed to show a hierarchical structure of `UnitCell`s,
    `Site`s, and `State`s. It allows for easy navigation and selection of these
    elements. The tree is built using a `QStandardItemModel`, and each item
    in the tree is represented by a `QStandardItem`. The tree supports
    single selection mode and can be edited by double-clicking on an item.
    The tree view emits a signal when the selection changes, providing
    information about the selected `UnitCell`, `Site`, and `State`.

    Attributes
    ----------
    tree_model : QStandardItemModel
        The model used to populate the tree view.
    root_node : QStandardItem
        The root item of the tree model, representing the tree's top level.
    tree_selection_changed : Signal
        Emitted when the selection in the tree changes. The signal carries
        a dictionary with the selected `UnitCell`, `Site`, and `State` IDs.

    Methods
    -------
    add_tree_item(name, uc_id, site_id=None, state_id=None)
        Add and select a tree item without rebuilding the entire tree.
    find_item_by_id(self, uc_id, site_id=None, state_id=None)
        Find a tree item by its ID.
    refresh_tree(unit_cells: dict[uuid.UUID, UnitCell])
        Rebuilds the entire tree from the current data model.
    remove_tree_item(uc_id, site_id=None, state_id=None)
        Remove an item from the tree.
    """

    tree_selection_changed = Signal(object)

    def __init__(self):
        super().__init__()
        self.setHeaderHidden(True)
        self.setSelectionMode(QTreeView.SingleSelection)
        self.setEditTriggers(QTreeView.DoubleClicked)

        # Create model
        self.tree_model = QStandardItemModel()
        self.root_node = self.tree_model.invisibleRootItem()

        # Set model to view
        self.setModel(self.tree_model)

        # Internal signals
        self.selectionModel().selectionChanged.connect(
            self._on_tree_selection_changed
        )

    def refresh_tree(self, unit_cells: dict[uuid.UUID, UnitCell]):
        """
        Rebuild the entire tree from the current data model.

        This method clears the existing tree and reconstructs it based on the
        current state of the unit_cells dictionary. It creates a hierarchical
        structure with three levels: unit cells, sites, and states.

        Note: For better performance, prefer the more specific update methods:
        - `add_tree_item()` - For adding or updating a single node
        - `remove_tree_item()` - For removing a single node

        This full refresh is typically only needed during initialization or
        when multiple components of the tree need to be updated simultaneously.

        Parameters
        ----------
        unit_cells : dict[uuid.UUID, UnitCell]
            Dictionary of `UnitCells`s to be displayed in the tree.
            The keys are UUIDs and the values are `UnitCell` objects.
        """
        self.tree_model.clear()
        self.root_node = self.tree_model.invisibleRootItem()

        # Add special "add" item at the top
        add_item = QStandardItem("+ Add Unit Cell")
        add_item.setData("ADD_UNIT_CELL", Qt.UserRole)  # Special marker
        add_item.setFlags(Qt.ItemIsEnabled)  # Not selectable, but clickable
        self.root_node.appendRow(add_item)

        # Add unit cells
        for uc_id, unit_cell in unit_cells.items():
            unit_cell_item = self._create_tree_item(
                unit_cell.name, item_id=uc_id
            )
            self.root_node.appendRow(unit_cell_item)

            # Add sites for this unit cell
            for site_id, site in unit_cell.sites.items():
                site_item = self._create_tree_item(site.name, item_id=site_id)
                unit_cell_item.appendRow(site_item)

                # Add states for this site
                for state_id, state in site.states.items():
                    state_item = self._create_tree_item(
                        state.name, item_id=state_id
                    )
                    site_item.appendRow(state_item)

    def _create_tree_item(
        self, item_name: str, item_id: uuid.UUID
    ) -> QStandardItem:
        """
        Create a `QStandardItem` for tree.

        Parameters
        ----------
        item_name : str
            Name of the item.
        item_id : uuid.UUID
            id of the item.

        Returns
        -------
        QStandardItem
            The new tree item.
        """
        tree_item = QStandardItem(item_name)
        tree_item.setData(item_id, Qt.UserRole)  # Store the ID

        # Set other visual properties
        tree_item.setEditable(True)

        return tree_item

    def find_item_by_id(
        self, uc_id, site_id=None, state_id=None
    ) -> QStandardItem | None:
        """
        Find a tree item by its ID.

        Parameters
        ----------
        uc_id : uuid.UUID
            id of the `UnitCell`
        site_id : uuid.UUID, optional
            id of the `Site`
        state_id : uuid.UUID, optional
            id of the `State`

        Returns
        -------
        QStandardItem | None
            The required item if found, `None` otherwise.
        """
        if state_id is not None:
            parent = self.find_item_by_id(uc_id, site_id)
            item_id = state_id
        elif site_id is not None:
            parent = self.find_item_by_id(uc_id)
            item_id = site_id
        else:
            parent = self.root_node
            item_id = uc_id

        for row in range(parent.rowCount()):
            item = parent.child(row)
            if item.data(Qt.UserRole) == item_id:
                return item

    def _select_item_by_id(self, uc_id, site_id=None, state_id=None) -> None:
        """
        Select a tree item programmatically by its ID.

        Parameters
        ----------
        uc_id : uuid.UUID
            id of the `UnitCell`
        site_id : uuid.UUID, optional
            id of the `Site`
        state_id : uuid.UUID, optional
            id of the `State`
        """
        item = self.find_item_by_id(uc_id, site_id, state_id)
        if item:
            index = self.tree_model.indexFromItem(item)
            self.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.ClearAndSelect
            )

    def add_tree_item(self, name, uc_id, site_id=None, state_id=None):
        """
        Add and select a tree item without rebuilding the entire tree.

        Parameters
        ----------
        uc_id : uuid.UUID
            id of the `UnitCell`
        site_id : uuid.UUID, optional
            id of the `Site`
        state_id : uuid.UUID, optional
            id of the `State`
        """
        if state_id is not None:  # Adding a state
            parent = self.find_item_by_id(uc_id, site_id)
            item_id = state_id
        elif site_id is not None:  # Adding a site
            parent = self.find_item_by_id(uc_id)
            item_id = site_id
        else:  # Adding a unit cell
            parent = self.root_node
            item_id = uc_id

        item = self._create_tree_item(name, item_id=item_id)
        parent.appendRow(item)
        index = self.tree_model.indexFromItem(item)
        self.selectionModel().setCurrentIndex(
            index, QItemSelectionModel.ClearAndSelect
        )

    def remove_tree_item(self, uc_id, site_id=None, state_id=None):
        """
        Remove an item from the tree.

        If the item has a parent
        (i.e., is not a `UnitCell`), select the parent. Otherwise,
        clear the selection

        Parameters
        ----------
        uc_id : uuid.UUID
            id of the `UnitCell`
        site_id : uuid.UUID, optional
            id of the `Site`
        state_id : uuid.UUID, optional
            id of the `State`
        """
        item = self.find_item_by_id(uc_id, site_id, state_id)
        if item:
            # If the item is a unit cell, the parent is None, so we
            # default to the invisibleRootItem
            parent = item.parent() or self.root_node
            # If the item has a parent, select it
            if item.parent():
                index = self.tree_model.indexFromItem(parent)
                self.selectionModel().setCurrentIndex(
                    index, QItemSelectionModel.ClearAndSelect
                )
            # Otherwise, deselect everything (the item is a unit cell)
            else:
                self.selectionModel().clearSelection()
                self.setCurrentIndex(QModelIndex())
            # Delete the item
            parent.removeRow(item.row())

    def _on_tree_selection_changed(self, selected: QStandardItem, deselected):
        """
        Handle the change of selection in the tree.

        This method is called when the user selects a node in the tree view or
        the selection occurs programmatically.
        It determines what type of node was selected
        (unit cell, site, or state) and emits a dictionary with
        the item's id and, if applicable, its parent's/grandparent's id's.
        The dictionary is then used to update the app's selection model.

        Parameters
        ----------
        selected : QStandardItem
            The newly selected item
        deselected : QStandardItem
            The previously selected items that are now deselected
        """
        indexes = selected.indexes()

        if not indexes:
            new_selection = {"unit_cell": None, "site": None, "state": None}
        else:
            # Get the selected item
            index = indexes[0]
            item = self.tree_model.itemFromIndex(index)

            item_id = item.data(Qt.UserRole)

            if item.parent() is None:  # unit cell selected
                new_selection = {
                    "unit_cell": item_id,
                    "site": None,
                    "state": None,
                }
            elif item.parent().parent() is None:  # site selected
                parent_item = item.parent()
                parent_id = parent_item.data(Qt.UserRole)

                new_selection = {
                    "unit_cell": parent_id,
                    "site": item_id,
                    "state": None,
                }
            else:  # state selected
                parent_item = item.parent()
                parent_id = parent_item.data(Qt.UserRole)
                grandparent_item = parent_item.parent()
                grandparent_id = grandparent_item.data(Qt.UserRole)

                new_selection = {
                    "unit_cell": grandparent_id,
                    "site": parent_id,
                    "state": item_id,
                }

        self.tree_selection_changed.emit(new_selection)

add_tree_item(name, uc_id, site_id=None, state_id=None)

Add and select a tree item without rebuilding the entire tree.

Parameters:

Name Type Description Default
uc_id UUID

id of the UnitCell

required
site_id UUID

id of the Site

None
state_id UUID

id of the State

None
Source code in TiBi/views/widgets/system_tree.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def add_tree_item(self, name, uc_id, site_id=None, state_id=None):
    """
    Add and select a tree item without rebuilding the entire tree.

    Parameters
    ----------
    uc_id : uuid.UUID
        id of the `UnitCell`
    site_id : uuid.UUID, optional
        id of the `Site`
    state_id : uuid.UUID, optional
        id of the `State`
    """
    if state_id is not None:  # Adding a state
        parent = self.find_item_by_id(uc_id, site_id)
        item_id = state_id
    elif site_id is not None:  # Adding a site
        parent = self.find_item_by_id(uc_id)
        item_id = site_id
    else:  # Adding a unit cell
        parent = self.root_node
        item_id = uc_id

    item = self._create_tree_item(name, item_id=item_id)
    parent.appendRow(item)
    index = self.tree_model.indexFromItem(item)
    self.selectionModel().setCurrentIndex(
        index, QItemSelectionModel.ClearAndSelect
    )

find_item_by_id(uc_id, site_id=None, state_id=None)

Find a tree item by its ID.

Parameters:

Name Type Description Default
uc_id UUID

id of the UnitCell

required
site_id UUID

id of the Site

None
state_id UUID

id of the State

None

Returns:

Type Description
QStandardItem | None

The required item if found, None otherwise.

Source code in TiBi/views/widgets/system_tree.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def find_item_by_id(
    self, uc_id, site_id=None, state_id=None
) -> QStandardItem | None:
    """
    Find a tree item by its ID.

    Parameters
    ----------
    uc_id : uuid.UUID
        id of the `UnitCell`
    site_id : uuid.UUID, optional
        id of the `Site`
    state_id : uuid.UUID, optional
        id of the `State`

    Returns
    -------
    QStandardItem | None
        The required item if found, `None` otherwise.
    """
    if state_id is not None:
        parent = self.find_item_by_id(uc_id, site_id)
        item_id = state_id
    elif site_id is not None:
        parent = self.find_item_by_id(uc_id)
        item_id = site_id
    else:
        parent = self.root_node
        item_id = uc_id

    for row in range(parent.rowCount()):
        item = parent.child(row)
        if item.data(Qt.UserRole) == item_id:
            return item

refresh_tree(unit_cells)

Rebuild the entire tree from the current data model.

This method clears the existing tree and reconstructs it based on the current state of the unit_cells dictionary. It creates a hierarchical structure with three levels: unit cells, sites, and states.

Note: For better performance, prefer the more specific update methods: - add_tree_item() - For adding or updating a single node - remove_tree_item() - For removing a single node

This full refresh is typically only needed during initialization or when multiple components of the tree need to be updated simultaneously.

Parameters:

Name Type Description Default
unit_cells dict[UUID, UnitCell]

Dictionary of UnitCellss to be displayed in the tree. The keys are UUIDs and the values are UnitCell objects.

required
Source code in TiBi/views/widgets/system_tree.py
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def refresh_tree(self, unit_cells: dict[uuid.UUID, UnitCell]):
    """
    Rebuild the entire tree from the current data model.

    This method clears the existing tree and reconstructs it based on the
    current state of the unit_cells dictionary. It creates a hierarchical
    structure with three levels: unit cells, sites, and states.

    Note: For better performance, prefer the more specific update methods:
    - `add_tree_item()` - For adding or updating a single node
    - `remove_tree_item()` - For removing a single node

    This full refresh is typically only needed during initialization or
    when multiple components of the tree need to be updated simultaneously.

    Parameters
    ----------
    unit_cells : dict[uuid.UUID, UnitCell]
        Dictionary of `UnitCells`s to be displayed in the tree.
        The keys are UUIDs and the values are `UnitCell` objects.
    """
    self.tree_model.clear()
    self.root_node = self.tree_model.invisibleRootItem()

    # Add special "add" item at the top
    add_item = QStandardItem("+ Add Unit Cell")
    add_item.setData("ADD_UNIT_CELL", Qt.UserRole)  # Special marker
    add_item.setFlags(Qt.ItemIsEnabled)  # Not selectable, but clickable
    self.root_node.appendRow(add_item)

    # Add unit cells
    for uc_id, unit_cell in unit_cells.items():
        unit_cell_item = self._create_tree_item(
            unit_cell.name, item_id=uc_id
        )
        self.root_node.appendRow(unit_cell_item)

        # Add sites for this unit cell
        for site_id, site in unit_cell.sites.items():
            site_item = self._create_tree_item(site.name, item_id=site_id)
            unit_cell_item.appendRow(site_item)

            # Add states for this site
            for state_id, state in site.states.items():
                state_item = self._create_tree_item(
                    state.name, item_id=state_id
                )
                site_item.appendRow(state_item)

remove_tree_item(uc_id, site_id=None, state_id=None)

Remove an item from the tree.

If the item has a parent (i.e., is not a UnitCell), select the parent. Otherwise, clear the selection

Parameters:

Name Type Description Default
uc_id UUID

id of the UnitCell

required
site_id UUID

id of the Site

None
state_id UUID

id of the State

None
Source code in TiBi/views/widgets/system_tree.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
def remove_tree_item(self, uc_id, site_id=None, state_id=None):
    """
    Remove an item from the tree.

    If the item has a parent
    (i.e., is not a `UnitCell`), select the parent. Otherwise,
    clear the selection

    Parameters
    ----------
    uc_id : uuid.UUID
        id of the `UnitCell`
    site_id : uuid.UUID, optional
        id of the `Site`
    state_id : uuid.UUID, optional
        id of the `State`
    """
    item = self.find_item_by_id(uc_id, site_id, state_id)
    if item:
        # If the item is a unit cell, the parent is None, so we
        # default to the invisibleRootItem
        parent = item.parent() or self.root_node
        # If the item has a parent, select it
        if item.parent():
            index = self.tree_model.indexFromItem(parent)
            self.selectionModel().setCurrentIndex(
                index, QItemSelectionModel.ClearAndSelect
            )
        # Otherwise, deselect everything (the item is a unit cell)
        else:
            self.selectionModel().clearSelection()
            self.setCurrentIndex(QModelIndex())
        # Delete the item
        parent.removeRow(item.row())