class BPWidget(QWidget): def __init__(self, patient_id, patient_ids): QWidget.__init__(self) self.disabled = False self.chart_range = (-1, -1) self.visible_rows = -1 self.patient_id = patient_id self.table = self.load_contents( patient_ids[patient_id] if patient_id else {}) self.combo = QComboBox(self) if not self.patient_id: self.combo.addItem('') for id in patient_ids: self.combo.addItem(id_plus_name(id, patient_ids)) self.combo.setCurrentText(id_plus_name(patient_id, patient_ids)) self.combo.currentIndexChanged.connect(self.selection_change) # QWidget Layout self.main_layout = QHBoxLayout() self.left = QVBoxLayout() self.left.addWidget(self.combo, 1) self.left.addWidget(self.table, 7) self.table.verticalScrollBar().valueChanged.connect(self.scroll_change) self.main_layout.addLayout(self.left, 1) self.bpcanvas, self.pulcanvas = self.load_charts( self.data[len(self.data) - 24:] if len(self.data) > 24 else self.data, patient_ids[patient_id] if patient_id else {}) self.figures = QSplitter(Qt.Vertical) self.figures.addWidget(self.bpcanvas) self.figures.addWidget(self.pulcanvas) self.main_layout.addWidget(self.figures, 4) # Set the layout to the QWidget self.setLayout(self.main_layout) self.adjustSize() def load_contents(self, patient_info): if self.patient_id: self.data = bpm_db.read_measurements(self.patient_id) else: self.data = [] return BloodPressureTable(self.data, patient_info) def load_charts(self, data, patient_info): SECS_PER_DAY = 60 * 60 * 24 ddates = [d['date'].toSecsSinceEpoch() for d in data] dates = [] xticks = [] for d in ddates: day = secs_at_midnight(d) if not xticks or xticks[-1] != day: xticks.append(day) dates.append(len(xticks) - 1 + (d - day) / SECS_PER_DAY) xlabels = [ QDateTime.fromSecsSinceEpoch(tck).toString(XAXIS_FMT) for tck in xticks ] xticks = [x for x in range(len(xticks))] bpfig = Figure(figsize=(800, 600), dpi=72, facecolor=(1, 1, 1), edgecolor=(0, 0, 0)) ax = bpfig.add_subplot(111) ax.plot(dates, [d['sys'] for d in data], 'b') ax.plot(dates, [d['dia'] for d in data], 'g') if dates: systolic_limit = bpm_db.SYSTOLIC_LIMIT if 'systolic_limit' in patient_info: systolic_limit = patient_info['systolic_limit'] diastolic_limit = bpm_db.DIASTOLIC_LIMIT if 'diastolic_limit' in patient_info: diastolic_limit = patient_info['diastolic_limit'] xaxis = [xticks[0], dates[-1]] if systolic_limit: ax.plot(xaxis, [systolic_limit] * 2, 'r:') if diastolic_limit: ax.plot(xaxis, [diastolic_limit] * 2, 'r:') ax.grid(True) ax.set_ylabel('mm Hg') ax.set_title('Blood Pressure') ax.set_xticks(xticks) ax.set_xticklabels(xlabels) pulfig = Figure(figsize=(800, 350), dpi=72, facecolor=(1, 1, 1), edgecolor=(0, 0, 0)) ax = pulfig.add_subplot(111) ax.plot(dates, [d['pulse'] for d in data], 'c') if dates: ax.grid(True) ax.set_ylabel('/ min') ax.set_title('Pulse') ax.set_xticks(xticks) ax.set_xticklabels(xlabels) return FigureCanvas(bpfig), FigureCanvas(pulfig) def selection_change(self, i): patient_id = self.combo.itemText(i).split(' ')[0] if not self.disabled and patient_id != self.patient_id: self.disabled = True self.parent().set_status_message(patient_id) self.parent().setCentralWidget( BPWidget(patient_id, self.parent().patient_ids)) def scroll_change(self): if not self.disabled: scroll_bar = self.table.verticalScrollBar() if scroll_bar.isVisible(): value = scroll_bar.value() if self.visible_rows == -1: self.visible_rows = 0 height = self.table.height() for i in range(len(self.data)): rect = self.table.visualItemRect(self.table.item(i, 1)) if rect.y() + rect.height() >= 0 and rect.y() < height: self.visible_rows += 1 if self.chart_range != (value, value + self.visible_rows): self.chart_range = (value, value + self.visible_rows) bpcanvas, pulcanvas = self.load_charts( self.data[value:value + self.visible_rows], self.parent().patient_ids[self.patient_id] if self.patient_id else {}) self.figures.replaceWidget(0, bpcanvas) self.figures.replaceWidget(1, pulcanvas) self.figures.refresh() self.bpcanvas = bpcanvas self.pulcanvas = pulcanvas
class ModelManager(QWidget): """ The main window for MolaQT. It manages optimisation models optionally using an openLCA database. """ def __init__(self, system): super().__init__() self.system = system # model config file self.controller_config_file = None # workflow for building model self.controller = QLabel() # db tree self.db_tree = QTreeWidget() self.db_tree.setHeaderLabels(['Database']) self.db_tree.setMinimumWidth(250) self.db_tree.itemDoubleClicked.connect(self.load_model) # context menu for db tree self.db_tree.setContextMenuPolicy(Qt.ActionsContextMenu) self.duplicate_model_action = QAction("Duplicate model") self.duplicate_model_action.triggered.connect(lambda: self.rename_model(copy=True)) self.db_tree.addAction(self.duplicate_model_action) self.rename_model_action = QAction("Rename model") self.rename_model_action.triggered.connect(self.rename_model) self.db_tree.addAction(self.rename_model_action) self.delete_model_action = QAction("Delete model") self.delete_model_action.triggered.connect(self.delete_model) self.db_tree.addAction(self.delete_model_action) # model configurations that don't use a database self.no_db = QTreeWidgetItem(self.db_tree, ['None']) self.no_db.setExpanded(True) # find the user sqlite databases and add them to db_tree self.db_items = {} db_files = list(system['data_path'].glob('*.sqlite')) for db_file in db_files: self.db_items[db_file] = QTreeWidgetItem(self.db_tree, [db_file.stem]) self.db_items[db_file].setExpanded(True) # add each model config to its database item by examining db_file entry config_item = [] for cf in system['config_path'].glob('*.json'): with open(str(cf)) as fp: config_json = json.load(fp) if 'db_file' in config_json and config_json['db_file'] is not None: config_db = Path(config_json['db_file']) if config_db.exists(): config_item.append(QTreeWidgetItem(self.db_items[config_db], [cf.stem])) else: config_item.append(QTreeWidgetItem(self.no_db, [cf.stem])) # arrange widgets in splitter box = QHBoxLayout() self.splitter = QSplitter() self.splitter.addWidget(self.db_tree) self.splitter.addWidget(self.controller) self.splitter.setStretchFactor(1, 2) box.addWidget(self.splitter) self.setLayout(box) def load_model(self, item, col): if self.db_tree.indexOfTopLevelItem(item) == -1: config_file = self.system['config_path'].joinpath(item.text(0)) logging.info('Loading model %s' % config_file) self.set_controller(config_file.with_suffix('.json')) def new_model(self): dialog = md.NewModelDialog(system=self.system, parent=self, db_files=self.db_items.keys()) if dialog.exec(): name, specification_class, controller_class, database, doc_file = dialog.get_inputs() config_file = self.system['config_path'].joinpath(name + '.json') if config_file.exists(): QMessageBox.about(self, "Error", "Configuration file " + str(config_file.absolute()) + " already exists") else: if database: item = QTreeWidgetItem(self.db_items[database], [config_file.stem]) else: item = QTreeWidgetItem(self.no_db, [config_file.stem]) self.db_tree.clearSelection() item.setSelected(True) self.controller_config_file = config_file # get a new config dict new_config = mqu.get_new_config(specification_class, database, doc_file, controller_class) # instantiate controller using config new_controller = controller_class(new_config, self.system) # open new controller widget self.replace_controller(new_controller) return config_file return None def save_model(self): try: if self.is_model_loaded(): config = self.controller.get_config() with open(str(self.controller_config_file), 'w') as fp: json.dump(config, fp, indent=4) self.controller.saved = True logging.info('Saved model configuration to %s' % self.controller_config_file) return self.controller_config_file else: logging.info("Nothing to save") except Exception as e: md.critical_error_box('Critical error', str(e)) return None def close_model(self): if self.controller_config_file is not None: choice = None if not self.controller.saved: choice = QMessageBox.question(self, 'Model not saved', "Confirm close?", QMessageBox.Yes | QMessageBox.No) if choice == QMessageBox.Yes or self.controller.saved: self.replace_controller(QLabel()) logging.info('Closed model %s' % self.controller_config_file) return True return False def build_model(self): if self.is_model_loaded(): # TODO: this requires the controller to have a model_build widget and button clicked method if hasattr(self.controller, 'model_build') and hasattr(self.controller.model_build, 'build_button_clicked'): ok = self.controller.model_build.build_button_clicked() return ok def run_model(self): if self.is_model_loaded(): # TODO: this requires the controller to have a model_solve widget and button clicked method if hasattr(self.controller, 'model_solve') and hasattr(self.controller.model_solve, 'run_button_clicked'): ok = self.controller.model_solve.run_button_clicked() return ok def start_console(self): self.qt_console = QtConsoleWindow(manager=self) self.qt_console.show() def delete_model(self): index = self.db_tree.selectedItems()[0] if index.parent() is not None: db_index = index.parent() model_name = index.text(0) choice = QMessageBox.question( self, 'Delete model', 'Confirm delete ' + model_name + ' from ' + db_index.text(0) + '?', QMessageBox.Yes | QMessageBox.No ) if choice == QMessageBox.Yes: db_index.removeChild(index) self.replace_controller(QLabel()) self.system['config_path'].joinpath(model_name).with_suffix('.json').unlink() logging.info("Deleted %s" % model_name) else: pass def rename_model(self, copy=False): index = self.db_tree.selectedItems()[0] if index.parent() is not None: db_index = index.parent() model_name = index.text(0) dialog = md.RenameModelDialog(current_model_name=model_name, parent=self) if dialog.exec(): old_config_path = self.system['config_path'].joinpath(model_name).with_suffix('.json') new_model_name = dialog.new_model_name.text() new_config_path = self.system['config_path'].joinpath(new_model_name).with_suffix('.json') if new_config_path.exists(): QMessageBox.about(self, "Error", "Configuration file " + str(new_config_path.absolute()) + " already exists") elif self.is_model_load() and not self.controller.saved: QMessageBox.about(self, "Error", "Model not saved") else: if self.controller is not None: self.replace_controller(QLabel()) if copy: new_config_path.write_text(old_config_path.read_text()) else: db_index.removeChild(index) old_config_path.rename(new_config_path) qtw = QTreeWidgetItem(db_index, [new_model_name]) db_index.addChild(qtw) self.db_tree.clearSelection() qtw.setSelected(True) logging.info('Renamed {} to {}'.format(model_name, dialog.new_model_name.text())) def set_controller(self, config_file): self.controller_config_file = config_file if self.parent() is not None: self.parent().setWindowTitle(config_file.stem + ' - molaqt') if not config_file.exists(): logging.error("Cannot find configuration file %s" % config_file) return False # get configuration with open(config_file) as fp: user_config = json.load(fp) # instantiate controller using config if available otherwise default to StandardController if 'controller' in user_config: search = re.search("<class '(.*?)\.(.*?)\.(.*?)'>", user_config['controller']) class_name = search.group(3) class_ = getattr(mc, class_name) new_controller = class_(user_config, self.system) else: new_controller = mc.StandardController(user_config, self.system) self.replace_controller(new_controller) return True def replace_controller(self, new_controller): self.controller.deleteLater() # ensures Qt webengine process gets shutdown self.splitter.replaceWidget(1, new_controller) self.splitter.update() self.splitter.setStretchFactor(1, 2) self.controller = new_controller def add_database(self, db_path): db_item = QTreeWidgetItem(self.db_tree, [db_path.stem]) self.db_items[db_path] = db_item db_item.setSelected(True) def is_model_loaded(self): return not isinstance(self.controller, QLabel)
class LandingWindow(QWidget): def __init__(self, parent: Optional[QWidget], account: Optional[Account], client: Client): super(QWidget, self).__init__(parent) self.__client = client self.__layout_manager = QVBoxLayout(self) self._placeholder_frame = QFrame() self.__menu_bar = MenuBar(self) # Introduce main view variables, as null optionals self._conversation_view: Optional[ConversationView] = None self.__friends_list: Optional[FriendsListView] = None self.__convs_list: Optional[ConversationsListView] = None self.__splitter: Optional[QSplitter] = None self.__boot_thread: Optional[BootThread] = None self.__stack: Optional[QStackedWidget] = None if account: self.load_main_view(account) # Connect signals to slots self.__client.chat_received_signal.connect( self.handle_chat_received) else: self.account_creation = AccountCreationPresenter(self) self.__layout_manager.addWidget(self.account_creation) # Connect signals to slots self.account_creation.should_load_main_app.connect( self.load_main_view) def __del__(self): # FIXME: Deletion isnt working due to QThread throwing an exception on early termination pass def load_main_view(self, account: Account): self.__friends_list = FriendsListView(self, self.__client) self.__convs_list = ConversationsListView(self, self.__client) self._placeholder_frame = QFrame() self.__splitter = QSplitter(Qt.Horizontal) self.__boot_thread = BootThread(self, account) self.__boot_thread.upnp_exception_raised.connect( self.handle_upnp_exception) self.__boot_thread.start() self.__build_main_view() # Connect signals to slots self.__menu_bar.at_index(1).show_friends_signal.connect( lambda: self.__stack.setCurrentIndex(0)) self.__menu_bar.at_index(1).show_conversations_signal.connect( lambda: self.__stack.setCurrentIndex(1)) self.__menu_bar.at_index(0).application_quit_signal.connect( lambda: self.parent().close()) def __build_main_view(self): # Set up left-side of splitter # Stack of left-side widgets self.__stack = QStackedWidget(self) conversation_list_widget = QFrame() conv_layout = QHBoxLayout(conversation_list_widget) conv_layout.addWidget(self.__convs_list) self.__stack.addWidget(self.__friends_list) self.__stack.addWidget(self.__convs_list) stack_labels = QListWidget(self) stack_labels.setViewMode(QListView.IconMode) stack_labels.setFixedWidth(55) stack_labels.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) stack_labels.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Add friend icon friend_fp = str(get_file_path(DataType.ICONS, file_name_str="user.svg")) friend_icon = load_themed_icon(friend_fp, QColorConstants.White) stack_labels.addItem( QListWidgetItem(friend_icon, None, stack_labels, 0)) # Add chat icon chat_fp = str( get_file_path(DataType.ICONS, file_name_str="comment.svg")) chat_icon = load_themed_icon(chat_fp, QColorConstants.White) stack_labels.addItem(QListWidgetItem(chat_icon, None, stack_labels, 0)) stack_labels.currentRowChanged.connect( lambda i: self.__stack.setCurrentIndex(i)) # Combine stack and labels in frame left_frame = QFrame() layout_manager = QHBoxLayout(left_frame) layout_manager.addWidget(stack_labels) layout_manager.addWidget(self.__stack) self.__splitter.addWidget(left_frame) self.__splitter.addWidget(self._placeholder_frame) self.__layout_manager.addWidget(self.__splitter) # Connect events self.__client.start_chat_signal.connect(self.chat_started) def menu_bar(self): return self.__menu_bar # SLOTS @QtCore.pyqtSlot(Peer) def chat_started(self, peer: Peer): """ :return: """ self.__convs_list.model().add_peer(peer) self._conversation_view = ConversationView(self, self.__client, peer, self.__friends_list.model(), self.__convs_list.model()) self.layout() self.__splitter.replaceWidget(1, self._conversation_view) @QtCore.pyqtSlot(Peer) def handle_chat_received(self, peer: Peer): """ Slot connected to client's chat_received_signal Used to alert the user of an incoming message when it is not for the current conversation :return: """ if not self._conversation_view: return if peer is not self._conversation_view.peer(): # The active conversation is different than the one receiving the message index = self.__convs_list.model().index_of(peer) if index is not None: model_index = self.__convs_list.model().index( index, 0, QModelIndex()) self.__convs_list.model().setData(model_index, QBrush(Qt.red), Qt.ForegroundRole) @QtCore.pyqtSlot(int, str) def handle_upnp_exception(self, err_code: int, err_msg: str): """ Slot connected to BootThread's upnp_exception_raised signal Given a UPnP error, notifies the user through an error dialog. :param err_code: Error code associated with error :param err_msg: Error message raised """ error_msg = QErrorMessage(self.__app) error_msg.showMessage("Error {}: {}".format(err_code, err_msg))
class ChartDialog(QDialog): """The chart dialog""" def __init__(self, parent): if Figure is None: raise ModuleNotFoundError super().__init__(parent) self.actions = ChartDialogActions(self) self.chart_templates_toolbar = ChartTemplatesToolBar(self) self.setWindowTitle("Chart dialog") self.setModal(True) self.resize(800, 600) self.parent = parent self.actions = ChartDialogActions(self) self.dialog_ui() def on_template(self): """Event handler for pressing a template toolbar button""" chart_template_name = self.sender().data() chart_template_path = MPL_TEMPLATE_PATH / chart_template_name try: with open(chart_template_path) as template_file: chart_template_code = template_file.read() except OSError: return self.editor.insertPlainText(chart_template_code) def dialog_ui(self): """Sets up dialog UI""" msg = "Enter Python code into the editor to the left. Globals " + \ "such as X, Y, Z, S are available as they are in the grid. " + \ "The last line must result in a matplotlib figure.\n \n" + \ "Pressing Apply displays the figure or an error message in " + \ "the right area." self.message = QTextBrowser(self) self.message.setText(msg) self.editor = SpellTextEdit(self) self.splitter = QSplitter(self) buttonbox = self.create_buttonbox() self.splitter.addWidget(self.editor) self.splitter.addWidget(self.message) self.splitter.setOpaqueResize(False) self.splitter.setSizes([9999, 9999]) # Layout layout = QVBoxLayout(self) layout.addWidget(self.chart_templates_toolbar) layout.addWidget(self.splitter) layout.addWidget(buttonbox) self.setLayout(layout) def apply(self): """Executes the code in the dialog and updates the canvas""" # Get current cell key = self.parent.grid.current code = self.editor.toPlainText() figure = self.parent.grid.model.code_array._eval_cell(key, code) if isinstance(figure, Figure): canvas = FigureCanvasQTAgg(figure) self.splitter.replaceWidget(1, canvas) canvas.draw() else: if isinstance(figure, Exception): self.message.setText("Error:\n{}".format(figure)) else: msg_text = "Error:\n{} has type '{}', " + \ "which is no instance of {}." msg = msg_text.format(figure, type(figure).__name__, Figure) self.message.setText(msg) self.splitter.replaceWidget(1, self.message) def create_buttonbox(self): """Returns a QDialogButtonBox with Ok and Cancel""" button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Apply | QDialogButtonBox.Cancel) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) button_box.button(QDialogButtonBox.Apply).clicked.connect(self.apply) return button_box