Coverage for src/scrilla/gui/widgets/components.py: 20%
254 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-18 18:14 +0000
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-18 18:14 +0000
1# This file is part of scrilla: https://github.com/chinchalinchin/scrilla.
3# scrilla is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License version 3
5# as published by the Free Software Foundation.
7# scrilla is distributed in the hope that it will be useful,
8# but WITHOUT ANY WARRANTY; without even the implied warranty of
9# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10# GNU General Public License for more details.
12# You should have received a copy of the GNU General Public License
13# along with scrilla. If not, see <https://www.gnu.org/licenses/>
14# or <https://github.com/chinchalinchin/scrilla/blob/develop/main/LICENSE>.
15"""
16A series of classes that all inherit from ``PySide6.QtWidgets.QWidget` and build up in sequence the necessary functionality to grab and validate user input, calculate and display results, etc. These widgets are not directly displayed on the GUI; rather, they are used as building blocks by the `scrilla.gui.functions` module, to create widgets specifically for application functions.
18All widgets have a similar structure in that they call a series of methods tailored to their specific class:
19 1. **_init**
20 Child widgets are constructed. Layouts are created.
21 2. **_arrange**
22 Child widgets are arranged. Layouts are applied.
23 3. **_stage**
24 Child widgets are prepped for display.
26The idea behind the module is to hide as much widget initialization and styling as possible behind its functions, so the `scrilla.gui.functions` module can focus on application-level logic and isn't cluttered with ugly declarative configuration.
28Widgets have styles applied to them through the `scrilla.styles.app.qss` stylesheet. Widgets are layered over one another in a hierarchy described by: `root` -> `child` -> `grand-child` -> `great-grand-child` -> etc. Each layers has a theme that descends down the material color scheme hex codes in sequence with its place in the layer hierarchy. See `scrilla.gui.formats` for more information.
29"""
30import datetime
31from typing import Callable, Dict, List, Union
32from PySide6 import QtCore, QtWidgets, QtGui
34from scrilla import settings
35from scrilla.util import helper
36from scrilla.gui import utilities
37from scrilla.static import definitions
38from scrilla.gui.widgets import factories
40SYMBOLS_LIST = "list"
41"""Constant passed into `scrilla.gui.widgets.components.ArgumentWidget` to initialize an input control allowing user to input list of ticker symbols"""
42SYMBOLS_SINGLE = "single"
43"""Constant passed into `scrilla.gui.widgets.components.ArgumentWidget` to initialize an input control allowing user to input single ticker symbol"""
44SYMBOLS_NONE = "none"
45"""Constant passed into `scrilla.gui.widgets.components.ArgumentWidget` to initialize an input control without ticker symbols"""
48class SkeletonWidget(QtWidgets.QWidget):
49 """
50 Base classfor other more complex widgets to inherit. An instance of `scrilla.gui.widgets.SkeletonWidget` generates an empty configuration attribute, i.e. a dictionary of booleans all set to their appropriate values for the given type of Widget and keyed to the available types of arguments for each function in `scrilla`, e.g., each instance of `scrilla.gui.widgets.SkeletonWidget` has the following attribute by default,
52 ```python
53 self.controls = {
54 'start_date': True,
55 'end_date': True,
56 'target': False,
57 # ...
58 }
59 ```
61 Where the value of the boolean is determined by the type of widget being constructed. The keys and values of the `self.controls` dictionary are generated from `scrilla.static.definitions.FUNC_DICT` and `scrilla.static.definitions.ARG_DICT`, filtering on the `cli_only` configuration option. In other words, the 'control skeleton' is generated by passing in a type of function and checking which types of arguments are restricted to the command line interface only.
63 Constructor
64 -----------
65 1. **function**: ``str``
66 Name of the function whose control skeleton is being constructed. Function names are accessible through the `scrilla.static.definitions.FUNC_DICT` dictionary keys.
67 2. **parent**: ``PySide6.QtWidgets.QWidget``
68 Parent of the widget.
69 """
70 def __init__(self, function: str, parent: QtWidgets.QWidget):
71 super(SkeletonWidget, self).__init__(parent)
72 self._configure_control_skeleton(function)
74 def _configure_control_skeleton(self, function: str):
75 """
76 Generates the control skeleton and stores it in `self.controls`.
78 Parameters
79 ----------
80 1. **function**: ``str``
81 Name of the function whose control is being constructed.
82 """
83 self.controls = factories.generate_control_skeleton()
85 for arg in definitions.FUNC_DICT[function]['args']:
86 if not definitions.ARG_DICT[arg]['cli_only']:
87 self.controls[arg] = True
90class ArgumentWidget(QtWidgets.QWidget):
91 """
92 Base class for other more complex widgets to inherit. An instance of `scrilla.gui.widgets.ArgumentWidget` embeds its child widgets in its own layout, which is an instance of``PySide6.QtWidgetQVBoxLayout``. This class provides access to the following widgets:a ``PySide6.QtWidgets.QLabel`` for the title widget, an error message widget, a ``PySide6.QtWidgets.QPushButton`` for the calculate button widget, a ``PySide6.QtWidget.QLineEdit`` for ticker symbol input, and a variety of optional input widgets enabled by boolean flags in the `controls` constructor argument.
94 Constructor
95 -----------
96 1. **calculate_function**: ``str``
97 Function attached to the `calculate_button` widget. Triggered when the widget is clicked.
98 2. **clear_function**: ``str``
99 Function attached to the `clear_button` widget. Trigged when the widget is clicked.
100 3. **controls:**: ``Dict[bool]``
101 Dictionary of boolean flags instructing the class which optional input to include. Optional keys can be found in `scrilla.static.definitions.ARG_DICT`. An dictionary skeleton with all the optional input disabled can be retrieved through `scrilla.gui.widgets.factories.generate_control_skeleton`.
102 4. **layer**: ``str``
103 Stylesheet property attached to widget.
104 5. **single**: ``bool``
105 Flag to limit symbol input to one ticker symbol. Calls to `get_symbol_input()` will return
107 Attributes
108 ----------
109 1. **title**: ``PySide6.QtWidgets.QLabel``
110 2. **required_title**: ``PySide6.QtWidgets.QLabel``
111 3. **optional_title**: ``PySide6.QtWidgets.QLabel``
112 4. **required_pane**: ``PySide6.QtWidgets.QLabel``
113 5. **optional_pane**: ``PySide6.QtWidgets.QLabel``
114 6. **message**: ``PySide6.QtWidgets.QLabel``
115 Label attached to `self.symbol_input`.
116 7. **error_message**: ``PySide6.QtWidget.QLabel``
117 Message displayed if there is an error thrown during calculation.
118 8. **calculate_button**: ``PySide6.QtWidget.QPushButton``
119 Empty button for super classes to inherit, primed to fire events on return presses
120 9. **clear_button**: ``PySide6.QtWidget.QPushButton``
121 Empty button for super classes to inherit, primed to fire events on return presses.
122 10. **symbol_widget**: ``PySide6.QtWidget.QLineEdit``
123 Empty text area for super classes to inherit
124 11. **control_widgets**: ``Dict[str,Union[PySide6.QtWidgets.QWidget,None]]
125 Dictionary containing the optional input widgets.
126 """
127 # TODO: calculate and clear should be part of THIS constructor, doesn't make sense to have other widgets hook them up.
129 def __init__(self, calculate_function: Callable, clear_function: Callable, controls: Dict[str, bool], layer, mode: str = SYMBOLS_LIST):
130 super().__init__()
131 self.layer = layer
132 self.controls = controls
133 self.control_widgets = {}
134 self.group_control_widgets = None
135 self.calculate_function = calculate_function
136 self.clear_function = clear_function
138 self._init_widgets(mode)
139 self._generate_group_widgets()
140 self._arrange_widgets()
141 self._stage_widgets()
143 def _init_widgets(self, mode: str) -> None:
144 """
145 Creates child widgets by calling factory methods from `scrilla.gui.widgets.factories`. This method will iterate over `self.controls` and initialize the optional input widget accordingly. `self.optional_pane` and `self.required_pane`, the container widgets for the input elements, are initialized with a style tag on the same layer as the `scrilla.gui.widgets.components.ArgumentWidget`.
146 """
147 self.title = factories.atomic_widget_factory(
148 component='subtitle', title='Function Input')
149 self.optional_title = factories.atomic_widget_factory(
150 component='label', title='Optional Arguments')
151 self.error_message = factories.atomic_widget_factory(
152 component='error', title="Error Message Goes Here")
153 self.calculate_button = factories.atomic_widget_factory(
154 component='calculate-button', title='Calculate')
155 self.clear_button = factories.atomic_widget_factory(
156 component='clear-button', title='Clear')
158 if mode == SYMBOLS_LIST:
159 self.required_title = factories.atomic_widget_factory(
160 component='label', title='Required Arguments')
161 self.symbol_hint = factories.atomic_widget_factory(
162 component='text', title="Separate Tickers With Commas")
163 self.required_pane = factories.layout_factory(
164 layout='vertical-box')
165 self.required_pane.setObjectName(self.layer)
166 self.symbol_widget = factories.argument_widget_factory(
167 component='symbols', title="Asset Tickers :", optional=False)
168 elif mode == SYMBOLS_SINGLE:
169 self.required_title = factories.atomic_widget_factory(
170 component='label', title='Required Argument')
171 self.symbol_hint = factories.atomic_widget_factory(
172 component='text', title="Enter a Single Symbol")
173 self.required_pane = factories.layout_factory(
174 layout='vertical-box')
175 self.required_pane.setObjectName(self.layer)
176 self.symbol_widget = factories.argument_widget_factory(
177 component='symbol', title="Symbol: ", optional=False)
178 else:
179 self.symbol_widget = None
181 self.group_definitions = None
183 # TODO: yes, yes, very clever...i think. still needs re-factored for clarity.
184 # what exactly is going on here?
187 for control in self.controls:
188 if self.controls[control]:
189 if definitions.ARG_DICT[control]["widget_type"] != "group":
190 self.control_widgets[control] = factories.argument_widget_factory(definitions.ARG_DICT[control]['widget_type'],
191 f'{definitions.ARG_DICT[control]["name"]} :',
192 optional=True)
193 else:
194 if self.group_definitions is None:
195 self.group_definitions = {}
196 self.group_definitions[control] = definitions.ARG_DICT[control]
198 # NOTE: at this point, the following should hold:
199 # ```python
200 # assert isinstance(self.group_definitions, dict[str, any])
201 # assert isinstance(defintions.ARG_DICT[control]['name'], str)
202 # assert isinstance(definitions.ARG_DICT[control], dict[str, any])
203 # ```
204 #
205 # i.e., `self.group_defintions` should look like,
206 # ```python`
207 # self.group_defintions = {
208 # 'moments' : {
209 # 'name': 'Moment Matching'
210 # 'values': ['-moments', '--moments', ...],
211 # 'description': '...',
212 # ...
213 # }
214 # }
215 # ```
217 else:
218 self.control_widgets[control] = None
220 self.optional_pane = factories.layout_factory(layout='vertical-box')
221 self.optional_pane.setObjectName(self.layer)
222 self.setLayout(QtWidgets.QVBoxLayout())
224 def _generate_group_widgets(self):
225 """
227 .. notes::
228 - `groups` is an intermediate dictionary used to group the definitions of each group into an array keyed to the group name, e.g.,
229 ```python
230 groups = {
231 'Estimation Method': [
232 {
233 'name': 'Moment Matching',
234 # ...
235 },
236 {
237 'name': 'Percentile Matching`,
238 # ...
239 },
240 # ...
241 ]
242 }
243 ```
244 """
245 if self.group_definitions is not None:
246 self.group_control_widgets = {}
247 groups = {}
248 for definition in self.group_definitions.values():
250 group_key = definition['group']
251 group_name = definitions.GROUP_DICT[group_key]
253 if group_name not in groups:
254 groups[group_name] = [definition]
255 else:
256 groups[group_name].append(definition)
258 # str, dict from `scrilla.static.definitions.ARG_DICT`
259 for group_name, group_def in groups.items():
260 control_names = [ single_def['name'] for single_def in group_def ]
261 self.group_control_widgets[group_name] = factories.group_widget_factory(
262 control_names, group_name)
264 def _arrange_widgets(self):
265 """
266 Arrange child widgets in their layouts and provides rendering hints. The `self.symbol_widget` is set into a ``PySide6.QtWidgets.QVBoxLayout`` named `self.required_pane`. The optional input widgets are set into a separate ``PySide6.QtWidgets.QVBoxLayout`` named `self.optional_pane`. `self.required_pane` and `self.optional_pane` are in turn set into a parent `PySide6.QtWidgets.QVBoxLayout``, along with `self.calculate_button` and `self.clear_button`. A strecth widget is inserted between the input widgets and the button widgets.
268 """
269 factories.set_policy_on_widget_list([self.title, self.optional_title, self.optional_pane],
270 QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum))
271 if self.symbol_widget is not None:
272 factories.set_policy_on_widget_list([self.symbol_hint, self.required_title, self.required_pane],
273 QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum))
274 self.setSizePolicy(QtWidgets.QSizePolicy(
275 QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding))
277 if self.symbol_widget is not None:
278 self.required_pane.layout().addWidget(self.required_title)
279 self.required_pane.layout().addWidget(self.symbol_hint)
280 self.required_pane.layout().addWidget(self.symbol_widget)
281 if self.group_control_widgets is not None:
282 for widget in self.group_control_widgets.values():
283 self.required_pane.layout().addWidget(widget)
285 self.optional_pane.layout().addWidget(self.optional_title)
286 for control_widget in self.control_widgets.values():
287 if control_widget is not None:
288 self.optional_pane.layout().addWidget(control_widget)
290 self.layout().addWidget(self.title)
291 self.layout().addWidget(self.error_message)
292 if self.symbol_widget is not None:
293 self.layout().addWidget(self.required_pane)
294 self.layout().addWidget(self.optional_pane)
295 self.layout().addStretch()
296 self.layout().addWidget(self.calculate_button)
297 self.layout().addWidget(self.clear_button)
299 def _stage_widgets(self):
300 """
301 Prepares child widgets for display. `self.clear_function` and `self.calculate` function are hooked into the `clicked` signal emitted from `self.clear_button` and `self.calculate_button`, respectively.
302 """
303 self.error_message.hide()
304 self.clear_button.clicked.connect(self.clear_function)
305 self.calculate_button.clicked.connect(self.calculate_function)
306 if self.symbol_widget is not None:
307 # NOTE: symbol widget is technically a layout in which the lineedit is abutted by a label, so need
308 # to pull the actual input element from the layout
309 self.symbol_widget.layout().itemAt(1).widget(
310 ).returnPressed.connect(self.calculate_function)
312 def get_symbol_input(self) -> Union[List[str], None]:
313 """
314 Returns the symbols inputted into the `PySide6.QtWidgets.QLineEdit` child of `symbol_widget`. If the `ArgumentWidget` has been initialized as a `scrilla.gui.widgets.components.SYMBOLS_LIST`, the method will return a list of inputted strings. If the `ArgumentWidget` has been initialied as a `scrilla.gui.widgets.components.SYMBOLS_SINGLE`, the method will return a list with a string as its single element. If the `ArgumentWidget` has been initialized as a `scrilla.gui.widgets.components.SYMBOLS_NONE`, this method will return `None`. In addition, in the first two cases, if no input has been entered, this method will return `None`.
315 """
316 if self.symbol_widget is not None:
317 return helper.split_and_strip(self.symbol_widget.layout().itemAt(1).widget().text())
318 return None
320 def get_control_input(self, control_widget_key: str) -> Union[datetime.date, str, bool, None]:
321 """
322 Get the value on the specified optional input widget. Optional keys are accessed through the keys of the `scrilla.static.definitions.ARG_DICT` dictionary.
324 If the widget is disabled or has been excluded altogether from the parent widget, i.e. a value of `False` was passed in through the constructor's `controls` arguments for that optional input widget, this method will return `None`.
325 """
326 if self.control_widgets[control_widget_key] is None:
327 return None
329 widget = self.control_widgets[control_widget_key].layout().itemAt(
330 1).widget()
332 if not widget.isEnabled():
333 return None
335 if type(widget) is QtWidgets.QDateEdit:
336 return widget.date().toPython()
337 if type(widget) is QtWidgets.QLineEdit:
338 if type(widget.validator()) is QtGui.QIntValidator:
339 return int(widget.text())
340 if type(widget.validator()) in [QtGui.QDoubleValidator, QtGui.QRegularExpressionValidator]:
341 return float(widget.text())
342 return widget.text()
343 if type(widget) is QtWidgets.QRadioButton:
344 return widget.isChecked()
346 def prime(self) -> None:
347 """
348 Enables user input on child widgets, except `clear_button` which is disabled.
349 """
350 self.clear_button.hide()
351 self.calculate_button.show()
352 if self.symbol_widget is not None:
353 self.symbol_widget.layout().itemAt(1).widget().setEnabled(True)
354 self.symbol_widget.layout().itemAt(1).widget().clear()
355 for control in self.control_widgets:
356 if self.control_widgets[control] is not None:
357 if type(self.control_widgets[control].layout().itemAt(1).widget()) != QtWidgets.QRadioButton:
358 self.control_widgets[control].layout().itemAt(
359 1).widget().clear()
360 self.control_widgets[control].layout().itemAt(
361 2).widget().setEnabled(True)
362 self.control_widgets[control].layout().itemAt(
363 2).widget().setCheckState(QtCore.Qt.Unchecked)
364 self.control_widgets[control].layout().itemAt(
365 1).widget().setEnabled(False)
366 # NOTE: have to set Widget1 to disabled last since the signal on the Widget2 is connected
367 # through CheckState to the enabled stated of Widget1, i.e. flipping the CheckState on
368 # Widget2 switches the enabled state of Widget1.
370 def fire(self) -> None:
371 """
372 Disables user input on child widgets, except `clear_button` which is enabled.
373 """
374 self.clear_button.show()
375 self.calculate_button.hide()
376 if self.symbol_widget is not None:
377 self.symbol_widget.layout().itemAt(1).widget().setEnabled(False)
378 for control in self.control_widgets:
379 if self.control_widgets[control] is not None:
380 self.control_widgets[control].layout().itemAt(
381 1).widget().setEnabled(False)
382 self.control_widgets[control].layout().itemAt(
383 2).widget().setEnabled(False)
386class TableWidget(QtWidgets.QWidget):
387 """
388 Base class for other more complex widgets to inherit. An instance of `scrilla.gui.widgets.TableWidget` embeds its child widgets in its own layout, which is an instance of``PySide6.QtWidgetQVBoxLayout``. This class provides access to the following widgets: a ``PySide6.QtWidgets.QLabel`` for the title widget, an error message widget and a ``PySide6.QtWidgets.QTableWidget`` for results display.
390 Parameters
391 ----------
392 1. **layer**: ``str``
393 Stylesheet property attached to widget.
394 2. **widget_title**: ``str``
395 *Optional*. Defaults to "Table Result". Title of the widget.
397 Attributes
398 ----------
399 1. **title**: ``PySide6.QtWidget.QLabel``
400 2.. **table**: ``PySide6.QtWidget.QTableWidget``
402 """
404 def __init__(self, layer, widget_title: str = "Table Result"):
405 super().__init__()
406 self.layer = layer
407 self._init_widgets(widget_title)
408 self._arrange_widgets()
409 self._stage_widgets()
411 def _init_widgets(self, widget_title: str) -> None:
412 """Creates child widgets and their layouts"""
413 self.title_container = factories.layout_factory(
414 layout='horizontal-box')
415 self.title = factories.atomic_widget_factory(
416 component='heading', title=widget_title)
417 self.download_button = factories.atomic_widget_factory(
418 component='download-button', title=None)
419 self.table = factories.atomic_widget_factory(
420 component='table', title=None)
421 self.setLayout(QtWidgets.QVBoxLayout())
423 def _arrange_widgets(self) -> None:
424 factories.set_policy_on_widget_list([self, self.title, self.title_container],
425 QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
426 QtWidgets.QSizePolicy.Minimum))
427 self.title_container.layout().addWidget(self.title)
428 self.title_container.layout().addWidget(self.download_button)
429 self.layout().addWidget(self.title_container)
430 self.layout().addWidget(self.table, 1)
432 def _stage_widgets(self) -> None:
433 self.table.hide()
434 self.download_button.hide()
435 self.download_button.clicked.connect(self.show_file_dialog)
437 def init_table(self, rows: List[str], columns: List[str]) -> None:
438 """
439 Initializes `table` for display. Number of rows and columns is determined by the length of the passed in lists.
441 Parameters
442 ----------
443 1. **rows**: ``List[str]``
444 List containing the row headers.
445 2. **columns**: ``List[str]``
446 List containing the column headers.
447 """
448 self.table.setRowCount(len(rows))
449 self.table.setColumnCount(len(columns))
450 self.table.setHorizontalHeaderLabels(columns)
451 self.table.setVerticalHeaderLabels(rows)
453 def show_table(self):
454 self.table.resizeColumnsToContents()
455 self.table.show()
456 self.download_button.show()
458 @QtCore.Slot()
459 def show_file_dialog(self) -> None:
460 file_path = factories.atomic_widget_factory(
461 component='save-dialog', title=f'(*.{settings.FILE_EXT})')
462 file_path.selectFile(f'table.{settings.FILE_EXT}')
463 filename = None
464 if file_path.exec_() == QtWidgets.QDialog.Accepted:
465 filename = file_path.selectedFiles()
466 if (
467 filename is not None
468 and len(filename) > 0
469 and settings.FILE_EXT == 'json'
470 ):
471 utilities.download_table_to_json(self.table, filename[0])
474class GraphWidget(QtWidgets.QWidget):
475 """
476 Base class for other more complex widgets to inherit. An instance of `scrilla.gui.widgets.GraphWidget` embeds its child widgets in its own layout, which is an instance of``PySide6.QtWidgetQVBoxLayout``. This class provides access to the following widgets: a ``PySide6.QtWidgets.QLabel`` for the title widget, an error message widget and a ``PySide6.QtWidgets.QTableWidget`` for results display.
478 The graph to be displayed should be stored in the `scrilla.settings.TEMP_DIR` directory before calling the `show_pixmap` method on this class. It will load the graph from file and convert it into a ``PySide6.QtGui.QPixMap``.
480 Parameters
481 ----------
482 1. **tmp_graph_key**: ``str``
483 The key of the file in the `scrilla.settings.TEMP_DIR` used to store the image of the graph.
484 2. **widget_title**: ``str``
485 *Optional*. Defaults to "Graph Results". Title of the widget.
487 Attributes
488 ----------
489 1. **tmp_graph_key**: ``str``
490 2. **title**: ``PySide6.QtWidget.QLabel``
491 3. **figure**: ``PySide6.QtWidget.QLabel``
492 """
494 def __init__(self, tmp_graph_key: str, layer: str, widget_title: str = "Graph Results"):
495 super().__init__()
496 self.layer = layer
497 self.tmp_graph_key = tmp_graph_key
498 self._init_widgets(widget_title)
499 self._arrange_widgets()
500 self._stage_widgets()
502 def _init_widgets(self, widget_title: str) -> None:
503 self.title_container = factories.layout_factory(
504 layout='horizontal-box')
505 self.title = factories.atomic_widget_factory(
506 component='heading', title=widget_title)
507 self.download_button = factories.atomic_widget_factory(
508 component='download-button', title=None)
509 self.figure = factories.atomic_widget_factory(
510 component='figure', title=None)
511 self.setLayout(QtWidgets.QVBoxLayout())
513 def _arrange_widgets(self) -> None:
514 factories.set_policy_on_widget_list([self, self.title, self.title_container],
515 QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
516 QtWidgets.QSizePolicy.Minimum))
517 self.title_container.layout().addWidget(self.title)
518 self.title_container.layout().addWidget(self.download_button)
519 self.layout().addWidget(self.title_container)
520 self.layout().addWidget(self.figure)
522 def _stage_widgets(self) -> None:
523 self.figure.hide()
524 self.download_button.hide()
525 self.download_button.clicked.connect(self.show_file_dialog)
527 def set_pixmap(self) -> None:
528 self.figure.setPixmap(utilities.generate_pixmap_from_temp(
529 self.width(), self.height(), self.tmp_graph_key))
530 self.figure.show()
531 self.download_button.show()
533 def clear(self):
534 self.figure.hide()
535 self.download_button.hide()
537 @QtCore.Slot()
538 def show_file_dialog(self) -> None:
539 file_path = factories.atomic_widget_factory(
540 component='save-dialog', title=f'(*.{settings.IMG_EXT})')
541 file_path.selectFile(f'{self.tmp_graph_key}.{settings.IMG_EXT}')
542 filename = None
543 if file_path.exec_() == QtWidgets.QDialog.Accepted:
544 filename = file_path.selectedFiles()
545 if filename is not None and len(filename) > 0:
546 utilities.download_tmp_to_file(self.tmp_graph_key, filename[0])
549class CompositeWidget(QtWidgets.QWidget):
550 """
551 Constructor
552 -----------
553 1. **tmp_graph_key**: ``str``
554 2. **widget_title**: ``str``
555 *Optional*. Defaults to "Results". Title of the widget.
556 3. **table_title**: ``Union[str,None]``
557 4. **graph_title**: ``Union[str,None]``
559 Attributes
560 ----------
561 1. **title**: ``PySide6.QtWidgets.QLabel``
562 2. **table_widget**: ``scrilla.gui.widgets.TableWidget``
563 3. **graph_widget**: ``scrilla.gui.widgets.GraphWidget``
564 4. **tab_widget**: ``PySide6.QtWidget.QTabWidget``
566 """
568 def __init__(self, tmp_graph_key: str, layer: str, widget_title: str = "Results",
569 table_title: Union[str, None] = None,
570 graph_title: Union[str, None] = None):
571 super().__init__()
572 self.setObjectName(layer)
573 self._init_widgets(widget_title=widget_title,
574 tmp_graph_key=tmp_graph_key)
575 self._arrange_widgets(graph_title=graph_title,
576 table_title=table_title)
578 def _init_widgets(self, widget_title: str, tmp_graph_key: str) -> None:
579 """Creates child widgets and their layouts"""
580 self.title = factories.atomic_widget_factory(
581 component='subtitle', title=widget_title)
583 self.table_widget = TableWidget(layer=self.objectName())
584 self.graph_widget = GraphWidget(
585 tmp_graph_key=tmp_graph_key, layer=self.objectName())
587 self.tab_widget = QtWidgets.QTabWidget()
589 self.setLayout(QtWidgets.QVBoxLayout())
591 def _arrange_widgets(self, graph_title: str, table_title: str) -> None:
592 self.title.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
593 QtWidgets.QSizePolicy.Minimum))
594 self.tab_widget.setSizePolicy(QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,
595 QtWidgets.QSizePolicy.Expanding))
596 self.title.setAlignment(QtCore.Qt.AlignLeft)
598 self.tab_widget.addTab(self.table_widget, table_title)
599 self.tab_widget.addTab(self.graph_widget, graph_title)
601 self.layout().addWidget(self.title)
602 self.layout().addWidget(self.tab_widget)