diff --git a/autopilot/core/terminal.py b/autopilot/core/terminal.py index 11a5f30e..88076c31 100644 --- a/autopilot/core/terminal.py +++ b/autopilot/core/terminal.py @@ -1,16 +1,15 @@ +"""Methods for running the Terminal GUI""" + import argparse import json import sys import os - import datetime import logging import threading from collections import OrderedDict as odict import numpy as np - from PySide2 import QtCore, QtGui, QtSvg, QtWidgets - from autopilot import prefs from autopilot.core import styles @@ -35,8 +34,6 @@ # init prefs for module access prefs.init(prefs_file) - - from autopilot.core.subject import Subject from autopilot.core.plots import Plot_Widget from autopilot.core.networking import Terminal_Station, Net_Node @@ -44,7 +41,7 @@ from autopilot.core.gui import Control_Panel, Protocol_Wizard, Weights, Reassign, Calibrate_Water, Bandwidth_Test from autopilot.core.loggers import init_logger - +# Try to import viz, but continue if that doesn't work IMPORTED_VIZ = False VIZ_ERROR = None try: @@ -123,6 +120,12 @@ def __init__(self): # store instance globals()['_TERMINAL'] = self + # Load settings + # These are stored in ~/.config/Autopilot/Terminal.conf + # Currently, the only setting is "geometry", but loading here + # in case we start to use other ones in the future + self.settings = QtCore.QSettings("Autopilot", "Terminal") + # networking self.node = None self.networking = None @@ -192,9 +195,6 @@ def __init__(self): #self.heartbeat(once=True) self.logger.info('Terminal Initialized') - - - def initUI(self): """ Initializes graphical elements of Terminal. @@ -205,40 +205,42 @@ def initUI(self): * :class:`.gui.Control_Panel` * :class:`.plots.Plot_Widget` """ - - - # set central widget + # Set central widget self.widget = QtWidgets.QWidget() self.setCentralWidget(self.widget) - - - # Start GUI + # Set the layout self.layout = QtWidgets.QGridLayout() self.layout.setSpacing(0) self.layout.setContentsMargins(0,0,0,0) self.widget.setLayout(self.layout) + # Set title self.setWindowTitle('Terminal') #self.menuBar().setFixedHeight(40) - # Main panel layout - #self.panel_layout.setContentsMargins(0,0,0,0) - - # Init toolbar - # File menu - # make menu take up 1/10 of the screen - winsize = app.desktop().availableGeometry() - + # This is the pixel resolution of the entire screen + screensize = app.primaryScreen().size() + + # This is the available geometry of the primary screen, excluding + # window manager reserved areas such as task bars and system menus. + primary_display = app.primaryScreen().availableGeometry() + + + ## Initalize the menuBar + # Linux: Set the menuBar to a fixed height + # Darwin: Don't worry about menuBar if sys.platform == 'darwin': bar_height = 0 else: - bar_height = (winsize.height()/30)+5 + bar_height = (primary_display.height()/30)+5 self.menuBar().setFixedHeight(bar_height) - + # Create a File menu self.file_menu = self.menuBar().addMenu("&File") self.file_menu.setObjectName("file") + + # Add "New Pilot" and "New Protocol" actions to File menu new_pilot_act = QtWidgets.QAction("New &Pilot", self, triggered=self.new_pilot) new_prot_act = QtWidgets.QAction("New Pro&tocol", self, triggered=self.new_protocol) #batch_create_subjects = QtWidgets.QAction("Batch &Create subjects", self, triggered=self.batch_subjects) @@ -247,8 +249,10 @@ def initUI(self): self.file_menu.addAction(new_prot_act) #self.file_menu.addAction(batch_create_subjects) - # Tools menu + # Create a Tools menu self.tool_menu = self.menuBar().addMenu("&Tools") + + # Add actions to Tools menu subject_weights_act = QtWidgets.QAction("View Subject &Weights", self, triggered=self.subject_weights) update_protocol_act = QtWidgets.QAction("Update Protocols", self, triggered=self.update_protocols) reassign_act = QtWidgets.QAction("Batch Reassign Protocols", self, triggered=self.reassign_protocols) @@ -258,12 +262,12 @@ def initUI(self): self.tool_menu.addAction(reassign_act) self.tool_menu.addAction(calibrate_act) - # Plots menu + # Create a Plots menu and add Psychometric Curve action self.plots_menu = self.menuBar().addMenu("&Plots") psychometric = QtGui.QAction("Psychometric Curve", self, triggered=self.plot_psychometric) self.plots_menu.addAction(psychometric) - # Tests menu + # Create a Tests menu and add a Test Bandwidth action self.tests_menu = self.menuBar().addMenu("Test&s") bandwidth_test_act = QtWidgets.QAction("Test Bandwidth", self, triggered=self.test_bandwidth) self.tests_menu.addAction(bandwidth_test_act) @@ -279,89 +283,69 @@ def initUI(self): self.data_panel = Plot_Widget() self.data_panel.init_plots(self.pilots.keys()) - - - # Logo goes up top - # https://stackoverflow.com/questions/25671275/pyside-how-to-set-an-svg-icon-in-qtreewidgets-item-and-change-the-size-of-the - - # - # pixmap_path = os.path.join(os.path.dirname(prefs.get('AUTOPILOT_ROOT')), 'graphics', 'autopilot_logo_small.svg') - # #svg_renderer = QtSvg.QSvgRenderer(pixmap_path) - # #image = QtWidgets.QImage() - # #self.logo = QtSvg.QSvgWidget() - # - # - # # set size, preserving aspect ratio - # logo_height = round(44.0*((bar_height-5)/44.0)) - # logo_width = round(139*((bar_height-5)/44.0)) - # - # svg_renderer = QtSvg.QSvgRenderer(pixmap_path) - # image = QtGui.QImage(logo_width, logo_height, QtGui.QImage.Format_ARGB32) - # # Set the ARGB to 0 to prevent rendering artifacts - # image.fill(0x00000000) - # svg_renderer.render(QtGui.QPainter(image)) - # pixmap = QtGui.QPixmap.fromImage(image) - # self.logo = QtWidgets.QLabel() - # self.logo.setPixmap(pixmap) - + # Set logo to corner widget if sys.platform != 'darwin': self.menuBar().setCornerWidget(self.logo, QtCore.Qt.TopRightCorner) self.menuBar().adjustSize() - #self.logo.load(pixmap_path) - # Combine all in main layout + # Add Control Panel and Data Panel to main layout #self.layout.addWidget(self.logo, 0,0,1,2) self.layout.addWidget(self.control_panel, 0,0,1,1) self.layout.addWidget(self.data_panel, 0,1,1,1) self.layout.setColumnStretch(0, 1) self.layout.setColumnStretch(1, 3) - # Set size of window to be fullscreen without maximization - # Until a better solution is found, if not set large enough, the pilot tabs will - # expand into infinity. See the Expandable_Tabs class - #pdb.set_trace() - screensize = app.desktop().screenGeometry() - winsize = app.desktop().availableGeometry() - - # want to subtract bounding title box, our title bar, and logo height. - # our y offset will be the size of the bounding title box - - # Then our tilebar - # multiply by three to get the inner (file, etc.) bar, the top bar (min, maximize, etc) - # and then the very top system tray bar in ubuntu - #titleBarHeight = self.style().pixelMetric(QtWidgets.QStyle.PM_TitleBarHeight, - # QtWidgets.QStyleOptionTitleBar(), self) * 3 - title_bar_height = screensize.height()-winsize.height() - - #titleBarHeight = bar_height*2 - # finally our logo - logo_height = bar_height - - - - winheight = winsize.height() - title_bar_height - logo_height # also subtract logo height - winsize.setHeight(winheight) - self.max_height = winheight - self.setGeometry(winsize) - self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) - - # Set heights on control panel and data panel - - - # move to primary display and show maximized - primary_display = app.desktop().availableGeometry(0) - self.move(primary_display.left(), primary_display.top()) - # self.resize(primary_display.width(), primary_display.height()) - # - self.control_panel.setMaximumHeight(winheight) - self.data_panel.setMaximumHeight(winheight) + + ## Set window size + # The window size behavior depends on TERMINAL_WINSIZE_BEHAVIOR pref + # If 'remember': restore to the geometry from the last close + # If 'maximum': restore to fill the entire screen + # If 'moderate': restore to a reasonable size of (1000, 400) pixels + terminal_winsize_behavior = prefs.get('TERMINAL_WINSIZE_BEHAVIOR') + + # Set geometry according to pref + if terminal_winsize_behavior == 'maximum': + # Set geometry to available geometry + self.setGeometry(primary_display) + + # Set SizePolicy to maximum + self.setSizePolicy( + QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) + + # Move to top left corner of primary display + self.move(primary_display.left(), primary_display.top()) + + # Also set the maximum height of each panel + self.control_panel.setMaximumHeight(primary_display.height()) + self.data_panel.setMaximumHeight(primary_display.height()) + + elif terminal_winsize_behavior == 'remember': + # Attempt to restore previous geometry + if self.settings.value("geometry") is None: + # It was never saved, for instance, this is the first time + # this app has been run + # So default to the moderate size + self.move(primary_display.left(), primary_display.top()) + self.resize(1000, 400) + else: + # It was saved, so restore the last geometry + self.restoreGeometry(self.settings.value("geometry")) + + else: + # The moderate size + self.move(primary_display.left(), primary_display.top()) + self.resize(1000, 400) + + ## Finalize some aesthetics # set stylesheet for main window self.setStyleSheet(styles.TERMINAL) # set fonts to antialias self.setFont(self.font().setStyleStrategy(QtGui.QFont.PreferAntialias)) + + ## Show, and log that initialization is complete self.show() logging.info('UI Initialized') @@ -399,7 +383,6 @@ def heartbeat(self, once=False): self.heartbeat_timer.daemon = True self.heartbeat_timer.start() - def toggle_start(self, starting, pilot, subject=None): """Start or Stop running the currently selected subject's task. Sends a message containing the task information to the concerned pilot. @@ -529,11 +512,6 @@ def l_state(self, value): #self.control_panel.panels[value['pilot']].button.set_state(value['state']) self.pilots[value['pilot']]['state'] = value['state'] - - - - - def l_handshake(self, value): """ Pilot is sending its IP and state on startup. @@ -721,7 +699,6 @@ def subject_protocols(self): return subjects_protocols - def reassign_protocols(self): """ Batch reassign protocols and steps. @@ -830,8 +807,6 @@ def plot_psychometric(self): if psychometric_dialog.result() != 1: return - - chart = viz.plot_psychometric(psychometric_dialog.plot_params) text, ok = QtGui.QInputDialog.getText(self, 'save plot?', 'what to call this thing') @@ -841,27 +816,9 @@ def plot_psychometric(self): #chart.serve() - - - - #viz.plot_psychometric(self.subjects_protocols) #result = psychometric_dialog.exec_() - - - - - - - - - - - - - - def closeEvent(self, event): """ When Closing the Terminal Window, close any running subject objects, @@ -871,6 +828,9 @@ def closeEvent(self, event): to explicitly kill it. """ + # Save the window geometry, to be optionally restored next time + self.settings.setValue("geometry", self.saveGeometry()) + # TODO: Check if any subjects are currently running, pop dialog asking if we want to stop # Close all subjects files @@ -884,15 +844,11 @@ def closeEvent(self, event): event.accept() +# Create the QApplication and run it +# Prefs were already loaded at the very top if __name__ == "__main__": - - #with open(prefs_file) as prefs_file_open: - # prefs = json.load(prefs_file_open) - app = QtWidgets.QApplication(sys.argv) #app.setGraphicsSystem("opengl") app.setStyle('GTK+') # Keeps some GTK errors at bay ex = Terminal() sys.exit(app.exec_()) - - diff --git a/autopilot/prefs.py b/autopilot/prefs.py index 8082c9f9..2dc43e6d 100644 --- a/autopilot/prefs.py +++ b/autopilot/prefs.py @@ -282,6 +282,15 @@ class Scopes(Enum): "default": str(_basedir / "pilot_db.json"), "scope": Scopes.TERMINAL }, + 'TERMINAL_WINSIZE_BEHAVIOR': { + 'type': 'str', + 'text': ( + 'How the Terminal window is sized: ' + '"maximum", "moderate", or "remember"' + ), + 'default': 'moderate', + "scope": Scopes.TERMINAL + }, 'LINEAGE': { 'type': 'choice', "text": "Are we a parent or a child?", @@ -345,7 +354,6 @@ class Scopes(Enum): 'depends': 'AUDIOSERVER', "scope": Scopes.AUDIO }, - }) """ Ordered Dictionary containing default values for prefs.