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

1# This file is part of scrilla: https://github.com/chinchalinchin/scrilla. 

2 

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. 

6 

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. 

11 

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. 

17 

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. 

25 

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. 

27 

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 

33 

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 

39 

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""" 

46 

47 

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, 

51 

52 ```python 

53 self.controls = { 

54 'start_date': True, 

55 'end_date': True, 

56 'target': False, 

57 # ... 

58 } 

59 ``` 

60 

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. 

62 

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) 

73 

74 def _configure_control_skeleton(self, function: str): 

75 """ 

76 Generates the control skeleton and stores it in `self.controls`. 

77 

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() 

84 

85 for arg in definitions.FUNC_DICT[function]['args']: 

86 if not definitions.ARG_DICT[arg]['cli_only']: 

87 self.controls[arg] = True 

88 

89 

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.  

93 

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  

106 

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. 

128 

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 

137 

138 self._init_widgets(mode) 

139 self._generate_group_widgets() 

140 self._arrange_widgets() 

141 self._stage_widgets() 

142 

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') 

157 

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 

180 

181 self.group_definitions = None 

182 

183 # TODO: yes, yes, very clever...i think. still needs re-factored for clarity. 

184 # what exactly is going on here? 

185 

186 

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] 

197 

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 # ``` 

216 

217 else: 

218 self.control_widgets[control] = None 

219 

220 self.optional_pane = factories.layout_factory(layout='vertical-box') 

221 self.optional_pane.setObjectName(self.layer) 

222 self.setLayout(QtWidgets.QVBoxLayout()) 

223 

224 def _generate_group_widgets(self): 

225 """ 

226  

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(): 

249 

250 group_key = definition['group'] 

251 group_name = definitions.GROUP_DICT[group_key] 

252 

253 if group_name not in groups: 

254 groups[group_name] = [definition] 

255 else: 

256 groups[group_name].append(definition) 

257 

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) 

263 

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.  

267 

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)) 

276 

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) 

284 

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) 

289 

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) 

298 

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) 

311 

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 

319 

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. 

323 

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 

328 

329 widget = self.control_widgets[control_widget_key].layout().itemAt( 

330 1).widget() 

331 

332 if not widget.isEnabled(): 

333 return None 

334 

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() 

345 

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. 

369 

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) 

384 

385 

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.  

389 

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. 

396 

397 Attributes 

398 ---------- 

399 1. **title**: ``PySide6.QtWidget.QLabel`` 

400 2.. **table**: ``PySide6.QtWidget.QTableWidget`` 

401 

402 """ 

403 

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() 

410 

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()) 

422 

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) 

431 

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) 

436 

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. 

440 

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) 

452 

453 def show_table(self): 

454 self.table.resizeColumnsToContents() 

455 self.table.show() 

456 self.download_button.show() 

457 

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]) 

472 

473 

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.  

477 

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``. 

479 

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. 

486 

487 Attributes 

488 ---------- 

489 1. **tmp_graph_key**: ``str`` 

490 2. **title**: ``PySide6.QtWidget.QLabel`` 

491 3. **figure**: ``PySide6.QtWidget.QLabel`` 

492 """ 

493 

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() 

501 

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()) 

512 

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) 

521 

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) 

526 

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() 

532 

533 def clear(self): 

534 self.figure.hide() 

535 self.download_button.hide() 

536 

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]) 

547 

548 

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]`` 

558 

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`` 

565 

566 """ 

567 

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) 

577 

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) 

582 

583 self.table_widget = TableWidget(layer=self.objectName()) 

584 self.graph_widget = GraphWidget( 

585 tmp_graph_key=tmp_graph_key, layer=self.objectName()) 

586 

587 self.tab_widget = QtWidgets.QTabWidget() 

588 

589 self.setLayout(QtWidgets.QVBoxLayout()) 

590 

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) 

597 

598 self.tab_widget.addTab(self.table_widget, table_title) 

599 self.tab_widget.addTab(self.graph_widget, graph_title) 

600 

601 self.layout().addWidget(self.title) 

602 self.layout().addWidget(self.tab_widget)