Newer
Older
Contains the scheduler-gui MainWindow class for scheduling operations in an SFG.
from typing import Any, Iterable, List, Sequence, Type, Dict
from importlib.machinery import SourceFileLoader
import inspect
from qtpy.QtCore import QCoreApplication, Qt, Slot, Signal, QSettings, QStandardPaths
QApplication, QMainWindow, QMessageBox, QFileDialog, QInputDialog, QCheckBox, QAbstractButton,
QRect, QRectF, QPoint, QSize, QByteArray, QMarginsF, QObject)
from qtpy.QtGui import (
QPaintEvent, QPainter, QPainterPath, QColor, QBrush, QPen, QFont, QPolygon, QIcon, QPixmap,
QLinearGradient)
QGraphicsView, QGraphicsScene, QGraphicsWidget, QGraphicsScale,
QGraphicsLayout, QGraphicsLinearLayout, QGraphicsGridLayout, QGraphicsLayoutItem, QGraphicsAnchorLayout,
QGraphicsItem, QGraphicsItemGroup, QGraphicsRectItem, QHeaderView)
# if sys.version_info >= (3, 9):
# List = list
# #Dict = dict
if __debug__:
log.setLevel('DEBUG')
# Print some system version information
QT_API = os.environ.get('QT_API')
log.debug('Qt version (runtime): {}'.format(QtCore.qVersion()))
log.debug('Qt version (compiletime): {}'.format(QtCore.__version__))
log.debug('QT_API: {}'.format(QT_API))
if QT_API.lower().startswith('pyside'):
import PySide2
log.debug('PySide version: {}'.format(PySide2.__version__))
if QT_API.lower().startswith('pyqt'):
log.debug('PyQt version: {}'.format(PYQT_VERSION_STR))
log.debug('QtPy version: {}'.format(qtpy.__version__))
# Autocompile the .ui form to a python file.
try: # PyQt5, try autocompile
from qtpy.uic import compileUiDir
uic.compileUiDir('.', map=(lambda dir,file: (dir, 'ui_' + file)))
except:
try: # PySide2, try manual compile
import subprocess
for cmd in cmds:
subprocess.call(cmd.split())
else:
#TODO: Implement (startswith) 'win32', 'darwin' (MacOs)
raise SystemExit
except: # Compile failed, look for pre-compiled file
except: # Everything failed, exit
log.exception("Could not import 'Ui_MainWindow'.")
log.exception("Can't autocompile under", QT_API, "eviroment. Try to manual compile 'main_window.ui' to 'ui/main_window_ui.py'")
sys.path.insert(0, 'icons/') # Needed for the compiled '*_rc.py' files in 'ui_*.py' files
from ui_main_window import Ui_MainWindow # Only availible when the form (.ui) is compiled
# The folowing QCoreApplication values is used for QSettings among others
QCoreApplication.setOrganizationName('Linöping University')
QCoreApplication.setOrganizationDomain('liu.se')
QCoreApplication.setApplicationName('B-ASIC Scheduler')
#QCoreApplication.setApplicationVersion(__version__) # TODO: read from packet __version__
class MainWindow(QMainWindow, Ui_MainWindow):
self._table_items: Dict[str, QTableWidgetItem]= {
'schedule_time': QTableWidgetItem(), # Schedule related part
'cyclic': QTableWidgetItem(),
'resolution': QTableWidgetItem(),
'id': QTableWidgetItem(), # Component realtaed part
'name': QTableWidgetItem(),
'inports': QTableWidgetItem(),
'outports': QTableWidgetItem()}
log.debug('themeName: \'{}\''.format(QIcon.themeName()))
log.debug('themeSearchPaths: {}'.format(QIcon.themeSearchPaths()))
self.setupUi(self)
# self._splitter_pos = 0
self._read_settings()
self.menu_load_from_file.triggered .connect(self._load_schedule_from_pyfile)
self.menu_save .triggered .connect(self.save)
self.menu_save_as .triggered .connect(self.save_as)
self.menu_quit .triggered .connect(self.close)
self.menu_node_info .triggered .connect(self.toggle_component_info)
self.menu_exit_dialog .triggered .connect(self.toggle_exit_dialog)
self.splitter .splitterMoved .connect(self._splitter_moved)
# Setup event member functions
self.closeEvent = self._close_event
self.info_table.horizontalHeader().setDefaultAlignment(Qt.AlignLeft)
self.info_table.setHorizontalHeaderLabels(['Property','Value'])
# test = '#b085b2'
# self.info_table.setStyleSheet('alternate-background-color: lightGray;background-color: white;')
self.info_table.setStyleSheet('alternate-background-color: #fadefb;background-color: #ebebeb;')
self.info_table.setSpan(0, 0, 1, 2) # Span 'Schedule' over 2 columns
self.info_table.setSpan(1, 0, 1, 2) # Span 'Operator' over 2 columns
self.info_table.setItem(0, 0, QTableWidgetItem('Schedule'))
self.info_table.setItem(1, 0, QTableWidgetItem('Operator'))
self.info_table.item(0, 0).setBackground(Qt.gray)
self.info_table.item(1, 0).setBackground(Qt.gray)
self._splitter_min = self.splitter.minimumSizeHint().height()
self.splitter.setStretchFactor(0, 1)
self.splitter.setStretchFactor(1, 0)
self.splitter.setCollapsible(0, False)
self.splitter.setCollapsible(1, True)
self.view.setScene(self._scene)
self.view.scale(self._scale, self._scale)
self._scene.changed.connect(self.shrink_scene_to_min_size)
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
def fill_info_table_schedule(self, schedule: Schedule) -> None:
self._table_items['schedule_time'].setText(str(schedule.schedule_time))
self._table_items['cyclic'].setText(str(schedule.cyclic))
self._table_items['resolution'].setText(str(schedule.resolution))
self.info_table.insertRow(1)
self.info_table.insertRow(1)
self.info_table.insertRow(1)
self.info_table.setItem(1, 0, QTableWidgetItem('Schedule Time'))
self.info_table.setItem(2, 0, QTableWidgetItem('Cyclic'))
self.info_table.setItem(3, 0, QTableWidgetItem('Resolution'))
self.info_table.setItem(1, 1 ,self._table_items['schedule_time'])
self.info_table.setItem(2, 1 ,self._table_items['cyclic'])
self.info_table.setItem(3, 1 ,self._table_items['resolution'])
# self.info_table.setVerticalHeaderItem(0, mydict['test'])
# self._table_items['schedule_time'].setText('test item updated text')
for i in range(5,10):
self.info_table.insertRow(i)
item = QTableWidgetItem('this is a very very very very long string that says abolutly nothing')
self.info_table.setItem(i,0, QTableWidgetItem('property {}: '.format(i)))
self.info_table.setItem(i,1,item)
def fill_info_table_component(self, op: GraphComponent) -> None:
si = len(self._table_items) + 1 # si = start index
self.info_table.insertRow(si)
self.info_table.insertRow(si)
self.info_table.setItem(si + 0, 0, QTableWidgetItem('Graph ID'))
self.info_table.setItem(si + 0, 1, QTableWidgetItem(str(op.graph_id)))
self.info_table.setItem(si + 1, 0, QTableWidgetItem('Name'))
self.info_table.setItem(si + 1, 1, QTableWidgetItem(str(op.name)))
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
si += 2
params = [(k,v) for k,v in op.params]
for i in range(len(params)):
self.info_table.insertRow(si + i)
self.info_table.setItem(si + i, 0, QTableWidgetItem(params[i][0]))
self.info_table.setItem(si + i, 1, QTableWidgetItem(params[i][1]))
# graph_id
# name
# params (dict)
self._table_items['schedule_time'].setText(str(schedule.schedule_time))
self._table_items['cyclic'].setText(str(schedule.cyclic))
self._table_items['resolution'].setText(str(schedule.resolution))
self.info_table.insertRow(si)
self.info_table.insertRow(si)
self.info_table.insertRow(si)
self.info_table.setItem(si + 0, 0, QTableWidgetItem('ID'))
self.info_table.setItem(si + 1, 0, QTableWidgetItem('Name'))
self.info_table.setItem(si + 3, 0, QTableWidgetItem('Inports'))
self.info_table.setItem(si + 3, 0, QTableWidgetItem('Outports'))
self.info_table.setItem(si + 1, 1 ,self._table_items['id'])
self.info_table.setItem(si + 2, 1 ,self._table_items['name'])
self.info_table.setItem(si + 3, 1 ,self._table_items['inports'])
self.info_table.setItem(si + 3, 1 ,self._table_items['outports'])
# self.info_table.setVerticalHeaderItem(0, mydict['test'])
# self._table_items['schedule_time'].setText('test item updated text')
for i in range(5,10):
self.info_table.insertRow(i)
item = QTableWidgetItem('this is a very very very very long string that says abolutly nothing')
self.info_table.setItem(i,0, QTableWidgetItem('property {}: '.format(i)))
self.info_table.setItem(i,1,item)
def clear_info_table_schedule(self) -> None:
for _ in range(3): # Remove Schedule info
self.info_table.removeRow(1)
def clear_info_table_component(self) -> None:
for _ in range(5): # Remove component info
self.info_table.removeRow(2)
###############
#### Slots ####
###############
self._graph.schedule.plot_schedule()
print(f'filtersChildEvents(): {self._graph.filtersChildEvents()}')
def _load_schedule_from_pyfile(self) -> None:
settings = QSettings()
# open_dir = QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0] if not self._open_file_dialog_opened else ''
last_file = settings.value('mainwindow/last_opened_file', QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0], str)
if not os.path.exists(last_file): # if filename does not exist
last_file = os.path.dirname(last_file) + '/'
if not os.path.exists(last_file): # if path does not exist
last_file = QStandardPaths.standardLocations(QStandardPaths.HomeLocation)[0]
abs_path_filename, _ = QFileDialog.getOpenFileName(self,
if not abs_path_filename: # return if empty filename (QFileDialog was canceled)
return
log.debug('abs_path_filename = {}.'.format(abs_path_filename))
module_name = inspect.getmodulename(abs_path_filename)
if not module_name: # return if empty module name
log.error('Could not load module from file \'{}\'.'.format(abs_path_filename))
return
try:
module = SourceFileLoader(module_name, abs_path_filename).load_module()
except:
log.exception('Exception occurred. Could not load module from file \'{}\'.'.format(abs_path_filename))
return
schedule_obj_list = dict(inspect.getmembers(module, (lambda x: isinstance(x, Schedule))))
if not schedule_obj_list: # return if no Schedule objects in script
QMessageBox.warning(self,
self.tr('File not found'),
self.tr('Could not find any Schedule object in file \'{}\'.')
.format(os.path.basename(abs_path_filename)))
log.info('Could not find any Schedule object in file \'{}\'.'
.format(os.path.basename(abs_path_filename)))
return
ret_tuple = QInputDialog.getItem(self,
self.tr('Load object'),
self.tr('Found the following Schedule object(s) in file.)\n\n'
'Select an object to proceed:'),
schedule_obj_list.keys(),0,False)
if not ret_tuple[1]: # User canceled the operation
log.debug('Load schedule operation: user canceled')
settings.setValue("mainwindow/last_opened_file", abs_path_filename)
#@Slot()
def open(self, schedule: Schedule) -> None:
"""Takes in an Schedule and creates a GraphicsGraphItem object."""
self._graph = GraphicsGraphItem(schedule)
self._scene.addItem(self._graph)
self._graph.installSceneEventFilters(self._graph.event_items)
# graph.prepareGeometryChange()
# graph.setPos(200, 20)
# self._scene.setSceneRect(self._scene.itemsBoundingRect()) # Forces the scene to it's minimum size
# # Debug rectangles
# # self._scene.setSceneRect(self._scene.itemsBoundingRect()) # Forces the scene to it's minimum size
# m = QMarginsF(1/self._scale, 1/self._scale, 0, 0)
# m2 = QMarginsF(1, 1, 1, 1)
# pen.setCosmetic(True)
# for component in graph.items:
# self._scene.addRect(component.mapRectToScene(component.boundingRect() - m), pen)
# pen.setColor(Qt.red)
# for axis in graph.axes.childItems():
# self._scene.addRect(axis.mapRectToScene(axes.boundingRect() - m), pen)
# # self._scene.addRect(self._scene.itemsBoundingRect() - m, pen)
# self._scene.setSceneRect(self._scene.itemsBoundingRect())
self.update_statusbar(self.tr('Schedule loaded successfully'))
def save(self) -> None:
"""This method save an schedule."""
#TODO: all
self.update_statusbar(self.tr('Schedule saved successfully'))
@Slot()
def save_as(self) -> None:
"""This method save as an schedule."""
#TODO: all
self.printButtonPressed('save_schedule()')
self.update_statusbar(self.tr('Schedule saved successfully'))
"""This method toggles the right hand side info window."""
# Note: splitter handler index 0 is a hidden splitter handle far most left, use index 1
# settings = QSettings()
_, max = self.splitter.getRange(1) # tuple(min, max)
if self._splitter_pos < self._splitter_min:
self.splitter.moveSplitter(max - self._splitter_min, 1)
else:
self.splitter.moveSplitter(max - self._splitter_pos, 1)
s = QSettings()
s.setValue("mainwindow/hide_exit_dialog", checked)
"""Callback method used to check if the right widget (info window)
has collapsed. Update the checkbutton accordingly."""
if width == 0:
if self.menu_node_info.isChecked() is True:
self.menu_node_info.setChecked(False)
if self.menu_node_info.isChecked() is False:
self.menu_node_info.setChecked(True)
self._splitter_pos = width
def shrink_scene_to_min_size(self, region: List[QRectF]) -> None:
self._scene.setSceneRect(self._scene.itemsBoundingRect())
################
#### Events ####
################
"""Replaces QMainWindow default closeEvent(QCloseEvent) event"""
s = QSettings()
hide_dialog = s.value('mainwindow/hide_exit_dialog', False, bool)
ret = QMessageBox.StandardButton.Yes
if not hide_dialog:
box = QMessageBox(self)
box.setWindowTitle(self.tr('Confirm Exit'))
box.setText('<h3>' + self.tr('Confirm Exit') + '</h3><p><br>' +
self.tr('Are you sure you want to exit?') +
' <br></p>')
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
buttons: list[QAbstractButton] = box.buttons()
buttons[0].setText(self.tr('&Exit'))
buttons[1].setText(self.tr('&Cancel'))
if not hide_dialog:
s.setValue('mainwindow/hide_exit_dialog', checkbox.isChecked())
self._write_settings()
log.info('Exit: {}'.format(os.path.basename(__file__)))
#################################
#### Helper member functions ####
#################################
def printButtonPressed(self, func_name: str) -> None:
alert = QMessageBox(self)
alert.setText("Called from " + func_name + '!')
alert.exec_()
def update_statusbar(self, msg: str) -> None:
"""Write the given str to the statusbar with temporarily policy."""
def _write_settings(self) -> None:
"""Write settings from MainWindow to Settings."""
s = QSettings()
s.setValue('mainwindow/maximized', self.isMaximized()) # window: maximized, in X11 - alwas False
s.setValue('mainwindow/pos', self.pos()) # window: pos
s.setValue('mainwindow/size', self.size()) # window: size
s.setValue('mainwindow/state', self.saveState()) # toolbars, dockwidgets: pos, size
s.setValue('mainwindow/menu/node_info', self.menu_node_info.isChecked())
s.setValue('mainwindow/splitter/state', self.splitter.saveState())
s.setValue('mainwindow/splitter/pos', self.splitter.sizes()[1])
if s.isWritable():
log.debug('Settings written to \'{}\'.'.format(s.fileName()))
else:
log.warning('Settings cant be saved to file, read-only.')
def _read_settings(self) -> None:
"""Read settings from Settings to MainWindow."""
s = QSettings()
if s.value('mainwindow/maximized', defaultValue=False, type=bool):
self.showMaximized()
else:
self.move( s.value('mainwindow/pos', self.pos()))
self.resize( s.value('mainwindow/size', self.size()))
self.restoreState( s.value('mainwindow/state', QByteArray()))
self.menu_node_info.setChecked( s.value('mainwindow/menu/node_info', True, bool))
self.splitter.restoreState( s.value('mainwindow/splitter/state', QByteArray()))
self._splitter_pos = ( s.value('mainwindow/splitter/pos', 200, int))
self.menu_exit_dialog.setChecked( s.value('mainwindow/hide_exit_dialog', False, bool))
log.debug('Settings read from \'{}\'.'.format(s.fileName()))
sys.exit(app.exec_())
if __name__ == "__main__":
start_gui()