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
Esempio n. 2
0
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)
Esempio n. 3
0
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))
Esempio n. 4
0
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