Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[MAIN]
# PyQt6 is a compiled extension; let pylint import it to resolve names.
extension-pkg-allow-list=PyQt6

[MESSAGES CONTROL]
# Qt subclasses inherit their public API from the Qt base class.
disable=too-few-public-methods
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"unidesk.styles": ["*/*.qss", "*.svg"],
},
install_requires=["PyQt6"],
extras_require={"dev": ["ruff", "pylint"]},
entry_points={
'console_scripts': [
'unidesk = unidesk.main:main',
Expand Down
2 changes: 2 additions & 0 deletions src/unidesk/helpers/academic_config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Load and save the shared UniOS academic profile."""

import json
import os

Expand Down
7 changes: 5 additions & 2 deletions src/unidesk/helpers/autostart.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Per-user override of the UniDesk autostart entry."""

import os

# Disabling autostart drops a hidden override of the system-wide entry that
Expand All @@ -12,9 +14,10 @@


def is_autostart_disabled():
"""Return True if the user has written an override hiding the autostart entry."""
if not os.path.exists(AUTOSTART_PATH):
return False
with open(AUTOSTART_PATH) as f:
with open(AUTOSTART_PATH, encoding="utf-8") as f:
return "Hidden=true" in f.read()


Expand All @@ -25,5 +28,5 @@ def set_autostart(enabled):
os.remove(AUTOSTART_PATH)
else:
os.makedirs(os.path.dirname(AUTOSTART_PATH), exist_ok=True)
with open(AUTOSTART_PATH, "w") as f:
with open(AUTOSTART_PATH, "w", encoding="utf-8") as f:
f.write(_DISABLED_ENTRY)
5 changes: 4 additions & 1 deletion src/unidesk/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""UniDesk entry point."""

import os
import sys

Expand All @@ -8,6 +10,7 @@


def main():
"""Create the Qt application, show the welcome window and run it."""
app = QApplication(sys.argv)
app.setApplicationName("unidesk")
app.setDesktopFileName("unidesk")
Expand Down Expand Up @@ -37,4 +40,4 @@ def main():


if __name__ == "__main__":
main()
main()
87 changes: 46 additions & 41 deletions src/unidesk/ui/main_window.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""UniDesk main window."""

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QCheckBox,
Expand Down Expand Up @@ -31,6 +33,8 @@


class UniOSWelcome(QMainWindow):
"""Main window: a hero, the nav buttons, and a stack of subpages."""

def __init__(self):
super().__init__()
self.setWindowTitle(UI["window_title"])
Expand All @@ -53,7 +57,31 @@ def _build_main(self):
mp.setContentsMargins(0, 0, 0, 0)
mp.setSpacing(0)

# Hero
mp.addWidget(self._build_hero())
mp.addWidget(divider())

# Nav columns
nav_widget = QWidget()
nav_widget.setProperty("layout", "transparent")
nav_widget.setStyleSheet(load_qss("widgets/layout.qss"))
nav_layout = QHBoxLayout(nav_widget)
nav_layout.setContentsMargins(28, 24, 28, 24)
nav_layout.setSpacing(20)

left_col = self._build_nav_column(NAV_LEFT)
bottom_row, self._autostart_cb = self._build_bottom_row()
right_col = self._build_nav_column(NAV_RIGHT)

nav_layout.addLayout(left_col)
nav_layout.addLayout(right_col)
mp.addWidget(nav_widget, stretch=1)
mp.addLayout(bottom_row)

mp.addWidget(divider())
mp.addWidget(footer())
self._stack.addWidget(main_page) # index 0

def _build_hero(self):
hero = QWidget()
hero.setProperty("layout", "hero")
hero.setStyleSheet(load_qss("widgets/layout.qss"))
Expand All @@ -68,61 +96,38 @@ def _build_main(self):
hero_sub = qlabel(UI["hero_subtitle"], role="body")
hero_sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
hl.addWidget(hero_sub)
return hero

mp.addWidget(hero)
mp.addWidget(divider())

# Nav columns
nav_widget = QWidget()
nav_widget.setProperty("layout", "transparent")
nav_widget.setStyleSheet(load_qss("widgets/layout.qss"))
nav_layout = QHBoxLayout(nav_widget)
nav_layout.setContentsMargins(28, 24, 28, 24)
nav_layout.setSpacing(20)

left_col = QVBoxLayout()
left_col.setSpacing(10)
for key in NAV_LEFT:
def _build_nav_column(self, keys):
col = QVBoxLayout()
col.setSpacing(10)
for key in keys:
btn = NavButton(key)
btn.clicked.connect(lambda _, k=key: self._show_page(k))
left_col.addWidget(btn)
left_col.addStretch()
col.addWidget(btn)
col.addStretch()
return col

def _build_bottom_row(self):
bottom_row = QHBoxLayout()
bottom_row.setContentsMargins(28, 0, 28, 14)
self._autostart_cb = QCheckBox(UI["autostart_checkbox"])
self._autostart_cb.setChecked(True)
self._autostart_cb.setProperty("control", "autostart")
self._autostart_cb.setStyleSheet(load_qss("widgets/controls.qss"))
autostart_cb = QCheckBox(UI["autostart_checkbox"])
autostart_cb.setChecked(True)
autostart_cb.setProperty("control", "autostart")
autostart_cb.setStyleSheet(load_qss("widgets/controls.qss"))

self._autostart_cb.stateChanged.connect(self._toggle_autostart)
autostart_cb.stateChanged.connect(self._toggle_autostart)
cfg_btn = QPushButton(UI["configure_button"])
cfg_btn.setFixedHeight(28)
cfg_btn.setProperty("button", "configure")
cfg_btn.setStyleSheet(load_qss("widgets/controls.qss"))
cfg_btn.setCursor(Qt.CursorShape.PointingHandCursor)
cfg_btn.clicked.connect(self._show_academic_config)

bottom_row.addWidget(self._autostart_cb)
bottom_row.addWidget(autostart_cb)
bottom_row.addStretch()
bottom_row.addWidget(cfg_btn)

right_col = QVBoxLayout()
right_col.setSpacing(10)
for key in NAV_RIGHT:
btn = NavButton(key)
btn.clicked.connect(lambda _, k=key: self._show_page(k))
right_col.addWidget(btn)
right_col.addStretch()

nav_layout.addLayout(left_col)
nav_layout.addLayout(right_col)
mp.addWidget(nav_widget, stretch=1)
mp.addLayout(bottom_row)

mp.addWidget(divider())
mp.addWidget(footer())
self._stack.addWidget(main_page) # index 0
return bottom_row, autostart_cb

def _build_subpages(self):
subpages = {
Expand All @@ -136,7 +141,7 @@ def _build_subpages(self):
for key, widget in subpages.items():
self._page_indices[key] = self._stack.addWidget(widget)

def _toggle_autostart(self, state):
def _toggle_autostart(self, _state):
set_autostart(self._autostart_cb.isChecked())

def _show_page(self, key):
Expand Down
29 changes: 18 additions & 11 deletions src/unidesk/ui/pages/academic_config_page.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Page for choosing the student's university and department."""

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QComboBox, QPushButton

Expand All @@ -10,27 +12,32 @@
UNIVERSITIES = load("academic_institutions")["universities"]


def _academic_combo(placeholder, items=()):
"""Return a styled combo box holding `items`, with nothing selected."""
combo = QComboBox()
combo.setProperty("control", "academic")
combo.setStyleSheet(load_qss("widgets/controls.qss"))
combo.setPlaceholderText(placeholder)
combo.addItems(list(items))
combo.setCurrentIndex(-1)
return combo


def build_academic_config_page(on_back):
"""Return the academic profile page, pre-filled from the saved config."""
widget, cl = scroll_page(on_back, UI["academic_config_page_title"])

intro = qlabel(UI["academic_config_intro"], role="body", wrap=True)
cl.addWidget(intro)

cl.addWidget(qlabel(UI["academic_university_label"], role="field-label"))
university_combo = QComboBox()
university_combo.setProperty("control", "academic")
university_combo.setStyleSheet(load_qss("widgets/controls.qss"))
university_combo.setPlaceholderText(UI["academic_university_placeholder"])
university_combo.addItems(list(UNIVERSITIES.keys()))
university_combo.setCurrentIndex(-1)
university_combo = _academic_combo(
UI["academic_university_placeholder"], UNIVERSITIES.keys()
)
cl.addWidget(university_combo)

cl.addWidget(qlabel(UI["academic_department_label"], role="field-label"))
department_combo = QComboBox()
department_combo.setProperty("control", "academic")
department_combo.setStyleSheet(load_qss("widgets/controls.qss"))
department_combo.setPlaceholderText(UI["academic_department_placeholder"])
department_combo.setCurrentIndex(-1)
department_combo = _academic_combo(UI["academic_department_placeholder"])
cl.addWidget(department_combo)

status = qlabel("", role="muted", wrap=True)
Expand Down
14 changes: 4 additions & 10 deletions src/unidesk/ui/pages/credits_page.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
from PyQt6.QtWidgets import QFrame, QVBoxLayout
"""Credits page listing the project contributors."""

from ...helpers.text_data import load
from ...styles.loader import load_qss
from ..widgets import qlabel, scroll_page
from ..widgets import card, qlabel, scroll_page

CREDITS = load("credits")["people"]
UI = load("ui_strings")


def build_credits_page(on_back):
"""Return the credits page, with one card per contributor."""
widget, cl = scroll_page(on_back, UI["credits_page_title"])

for person in CREDITS:
frame = QFrame()
frame.setFrameShape(QFrame.Shape.StyledPanel)
frame.setProperty("layout", "card")
frame.setStyleSheet(load_qss("widgets/layout.qss"))
fl = QVBoxLayout(frame)
fl.setContentsMargins(14, 10, 14, 10)
fl.setSpacing(2)
frame, fl = card(spacing=2)

name = qlabel(person["name"], role="card-title")
fl.addWidget(name)
Expand Down
15 changes: 6 additions & 9 deletions src/unidesk/ui/pages/links_page.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
"""Useful links page."""

from PyQt6.QtCore import Qt, QUrl
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtWidgets import QFrame, QPushButton, QVBoxLayout
from PyQt6.QtWidgets import QPushButton

from ...helpers.text_data import load
from ...styles.loader import load_qss
from ..widgets import qlabel, scroll_page
from ..widgets import card, qlabel, scroll_page

LINKS = load("links")["links"]
UI = load("ui_strings")


def build_links_page(on_back):
"""Return the links page, with one card and Open button per link."""
widget, cl = scroll_page(on_back, UI["links_page_title"])

for link in LINKS:
frame = QFrame()
frame.setFrameShape(QFrame.Shape.StyledPanel)
frame.setProperty("layout", "card")
frame.setStyleSheet(load_qss("widgets/layout.qss"))
fl = QVBoxLayout(frame)
fl.setContentsMargins(14, 10, 14, 10)
fl.setSpacing(6)
frame, fl = card(spacing=6)

name = qlabel(link["label"], role="card-title")
fl.addWidget(name)
Expand Down
3 changes: 3 additions & 0 deletions src/unidesk/ui/pages/text_page.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Plain text subpages defined in pages.json."""

from PyQt6.QtCore import Qt

from ...helpers.text_data import pages
Expand All @@ -7,6 +9,7 @@


def build_text_page(key, on_back):
"""Return the scrollable text page for the pages.json entry `key`."""
data = PAGES[key]
widget, cl = scroll_page(on_back, key)

Expand Down
Loading