Skip to content

Commit

Permalink
Adds ability to save and reset layouts (#27)
Browse files Browse the repository at this point in the history
* working window save/load

* removed debug print and added tests

* changed test to adaptive width/height
  • Loading branch information
alexhroom authored Sep 11, 2024
1 parent 48af8a5 commit 23cfcbd
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 5 deletions.
19 changes: 18 additions & 1 deletion rascal2/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from enum import IntEnum, StrEnum
from os import PathLike
from pathlib import PurePath
from typing import Any
from typing import Any, TypeAlias

from pydantic import BaseModel, Field
from PyQt6 import QtCore
Expand Down Expand Up @@ -54,6 +54,20 @@ def _missing_(cls, value):
raise ValueError("Not a known logging level.")


# WindowGeometry is a tuple (x, y, width, height, minimized)
# where 'minimized' is True iff the window is minimized
WindowGeometry: TypeAlias = tuple[int, int, int, int, bool]


class MDIGeometries(BaseModel):
"""Model for storing window positions and sizes."""

plots: WindowGeometry = Field(max_length=5, min_length=5)
project: WindowGeometry = Field(max_length=5, min_length=5)
terminal: WindowGeometry = Field(max_length=5, min_length=5)
controls: WindowGeometry = Field(max_length=5, min_length=5)


class Settings(BaseModel, validate_assignment=True, arbitrary_types_allowed=True):
"""Model for system settings.
Expand All @@ -72,9 +86,12 @@ class Settings(BaseModel, validate_assignment=True, arbitrary_types_allowed=True
style: Styles = Field(default=Styles.Light, title="general", description="Style")
editor_fontsize: int = Field(default=12, title="general", description="Editor Font Size", gt=0)
terminal_fontsize: int = Field(default=12, title="general", description="Terminal Font Size", gt=0)

log_path: str = Field(default="logs/rascal.log", title="logging", description="Path to Log File")
log_level: LogLevels = Field(default=LogLevels.Info, title="logging", description="Minimum Log Level")

mdi_defaults: MDIGeometries | None = Field(default=None, title="windows", description="Default Window Geometries")

def model_post_init(self, __context: Any):
global_settings = get_global_settings()
unset_settings = [s for s in self.model_fields if s not in self.model_fields_set]
Expand Down
44 changes: 40 additions & 4 deletions rascal2/ui/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from PyQt6 import QtCore, QtGui, QtWidgets

from rascal2.config import path_for, setup_logging, setup_settings
from rascal2.core.settings import MDIGeometries
from rascal2.dialogs.project_dialog import ProjectDialog
from rascal2.widgets import ControlsWidget
from rascal2.widgets.startup_widget import StartUpWidget
Expand Down Expand Up @@ -107,9 +108,15 @@ def create_actions(self):

# Window menu actions
self.tile_windows_action = QtGui.QAction("Tile Windows", self)
self.tile_windows_action.setStatusTip("Arrange windows in the default grid")
self.tile_windows_action.setStatusTip("Arrange windows in the default grid.")
self.tile_windows_action.setIcon(QtGui.QIcon(path_for("tile.png")))
self.tile_windows_action.triggered.connect(self.mdi.tileSubWindows)
self.reset_windows_action = QtGui.QAction("Reset to Default")
self.reset_windows_action.setStatusTip("Reset the windows to their default arrangement.")
self.reset_windows_action.triggered.connect(self.reset_mdi_layout)
self.save_default_windows_action = QtGui.QAction("Save Current Window Positions")
self.save_default_windows_action.setStatusTip("Set the current window positions as default.")
self.save_default_windows_action.triggered.connect(self.save_mdi_layout)

def create_menus(self):
"""Creates the main menu and sub menus"""
Expand All @@ -129,6 +136,8 @@ def create_menus(self):

windows_menu = main_menu.addMenu("&Windows")
windows_menu.addAction(self.tile_windows_action)
windows_menu.addAction(self.reset_windows_action)
windows_menu.addAction(self.save_default_windows_action)

help_menu = main_menu.addMenu("&Help")
help_menu.addAction(self.open_help_action)
Expand Down Expand Up @@ -174,12 +183,39 @@ def setup_mdi(self):
widget, QtCore.Qt.WindowType.WindowMinMaxButtonsHint | QtCore.Qt.WindowType.WindowTitleHint
)
window.setWindowTitle(title)
# TODO implement user save for layouts, this should default to use saved layout and fallback to tile
# https://github.com/RascalSoftware/RasCAL-2/issues/15
self.mdi.tileSubWindows()
self.reset_mdi_layout()
self.startup_dlg = self.takeCentralWidget()
self.setCentralWidget(self.mdi)

def reset_mdi_layout(self):
"""Reset MDI layout to the default."""
if self.settings.mdi_defaults is None:
for window in self.mdi.subWindowList():
window.showNormal()
self.mdi.tileSubWindows()
else:
for window in self.mdi.subWindowList():
# get corresponding MDIGeometries entry for the widget
widget_name = window.windowTitle().lower().split(" ")[-1]
x, y, width, height, minimized = getattr(self.settings.mdi_defaults, widget_name)
if minimized:
window.showMinimized()
else:
window.showNormal()

window.setGeometry(x, y, width, height)

def save_mdi_layout(self):
"""Set current MDI geometries as the default."""
geoms = {}
for window in self.mdi.subWindowList():
# get corresponding MDIGeometries entry for the widget
widget_name = window.windowTitle().lower().split(" ")[-1]
geom = window.geometry()
geoms[widget_name] = (geom.x(), geom.y(), geom.width(), geom.height(), window.isMinimized())

self.settings.mdi_defaults = MDIGeometries.model_validate(geoms)

def init_settings_and_log(self, save_path: str):
"""Initialise settings and logging for the project.
Expand Down
64 changes: 64 additions & 0 deletions tests/test_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Unit tests for the main window view."""

from unittest.mock import patch

import pytest

from rascal2.core.settings import MDIGeometries, Settings
from rascal2.ui.view import MainWindowView


@pytest.mark.parametrize(
"geometry",
[
((1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True), (1, 2, 196, 24, True)),
((1, 2, 196, 24, True), (3, 78, 196, 24, True), (1, 2, 204, 66, False), (12, 342, 196, 24, True)),
],
)
@patch("rascal2.ui.view.MainWindowPresenter")
@patch("rascal2.ui.view.ControlsWidget.setup_controls")
class TestMDISettings:
def test_reset_mdi(self, mock1, mock2, geometry):
"""Test that resetting the MDI works."""
view = MainWindowView()
view.settings = Settings()
view.setup_mdi()
view.settings.mdi_defaults = MDIGeometries(
plots=geometry[0], project=geometry[1], terminal=geometry[2], controls=geometry[3]
)
view.reset_mdi_layout()
for window in view.mdi.subWindowList():
# get corresponding MDIGeometries entry for the widget
widget_name = window.windowTitle().lower().split(" ")[-1]
wgeom = window.geometry()
assert getattr(view.settings.mdi_defaults, widget_name) == (
wgeom.x(),
wgeom.y(),
wgeom.width(),
wgeom.height(),
window.isMinimized(),
)

def test_set_mdi(self, mock1, mock2, geometry):
"""Test that setting the MDI adds the expected object to settings."""
view = MainWindowView()
view.settings = Settings()
view.setup_mdi()
widgets_in_order = []

for i, window in enumerate(view.mdi.subWindowList()):
widgets_in_order.append(window.windowTitle().lower().split(" ")[-1])
window.setGeometry(*geometry[i][0:4])
if geometry[i][4] is True:
window.showMinimized()

view.save_mdi_layout()
for i, widget in enumerate(widgets_in_order):
window = view.mdi.subWindowList()[i]
assert getattr(view.settings.mdi_defaults, widget) == (
window.x(),
window.y(),
window.width(),
window.height(),
window.isMinimized(),
)

0 comments on commit 23cfcbd

Please sign in to comment.