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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.1] — 2026-07-25

### Fixed
- **Environment Variables: the ✕ button now removes the row** (#52). QML's
`model.removeRow(i)` was resolving to QAbstractItemModel's built-in C++
convenience instead of the same-named Python slot, landing in the default
`removeRows()` stub that does nothing. The model now overrides the
`removeRows()` virtual — the supported hook — so removal works and marks
the editor dirty.

### Docs
- Dropped the stale-release caveat from the README now that `v1.0.0` is
published.
Expand Down
5 changes: 5 additions & 0 deletions flatpak/io.github.i4ctime.protonshift.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
</developer>

<releases>
<release version="1.0.1" date="2026-07-25">
<description>
<p>Bug fix: the remove button in the Environment Variables editor now actually deletes the row.</p>
</description>
</release>
<release version="1.0.0" date="2026-07-22">
<description>
<p>First stable release of the native Qt Quick / PySide6 rewrite.</p>
Expand Down
2 changes: 1 addition & 1 deletion protonshift/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
domain logic lives in :mod:`protonshift.core`. No Electron, no web server.
"""

__version__ = "1.0.0"
__version__ = "1.0.1"
19 changes: 10 additions & 9 deletions protonshift/controllers/env_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,17 @@ def addRow(self) -> None: # noqa: N802
self.endInsertRows()
self.modified.emit()

# Properly overrides the QAbstractItemModel::removeRow(int, QModelIndex)
# virtual (compatible signature + bool return) instead of shadowing it with
# an incompatible Slot. QML's existing `model.removeRow(i)` calls still
# work — the extra parent argument defaults and the return is ignorable.
@Slot(int, result=bool)
def removeRow(self, row: int, parent: QModelIndex = QModelIndex()) -> bool: # noqa: N802
if parent.isValid() or not (0 <= row < len(self._rows)):
# QML's ✕ button calls `model.removeRow(i)`. Never define a Python method
# named `removeRow` here: QAbstractItemModel already exposes an invokable
# C++ removeRow(int, parent) convenience, and QML dispatches to that
# built-in — a same-named Python slot is unreachable from QML (#52: the
# ✕ button was a silent no-op). The convenience delegates to the
# *virtual* removeRows(), so that is the hook to override.
def removeRows(self, row: int, count: int, parent: QModelIndex = QModelIndex()) -> bool: # noqa: N802
if parent.isValid() or count < 1 or row < 0 or row + count > len(self._rows):
return False
self.beginRemoveRows(QModelIndex(), row, row)
del self._rows[row]
self.beginRemoveRows(QModelIndex(), row, row + count - 1)
del self._rows[row : row + count]
self.endRemoveRows()
self.modified.emit()
return True
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "protonshift"
version = "1.0.0"
version = "1.0.1"
description = "Native Qt Quick edition of ProtonShift — a Linux gaming setup tool"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
64 changes: 64 additions & 0 deletions tests/test_env_remove_row.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Regression test for issue #52: the env editor's ✕ button was a no-op.

QML calls ``model.removeRow(i)``. QAbstractItemModel already exposes that
name as an invokable C++ convenience, so a same-named Python slot is never
reached from QML — the call lands on the built-in, which delegates to the
virtual ``removeRows()``. The fix overrides ``removeRows``; this test drives
the call through a real QML engine (not Python) so the dispatch path that
broke is the one being tested. Runs headless via QT_QPA_PLATFORM=offscreen
(set in CI; forced here for local runs).
"""

from __future__ import annotations

import os

os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")

from PySide6.QtCore import QUrl # noqa: E402
from PySide6.QtGui import QGuiApplication # noqa: E402
from PySide6.QtQml import QQmlApplicationEngine # noqa: E402

from protonshift.controllers.env_controller import EnvVarsModel # noqa: E402

_QML = b"""
import QtQml
QtObject {
required property var model
function removeAt(i) { return model.removeRow(i) }
}
"""


def _qml_root(model: EnvVarsModel) -> tuple[QQmlApplicationEngine, object]:
# engine returned so it outlives the root object in the caller's scope
engine = QQmlApplicationEngine()
engine.setInitialProperties({"model": model})
engine.loadData(_QML, QUrl("test_env_remove_row.qml"))
roots = engine.rootObjects()
assert roots, "inline QML failed to load"
return engine, roots[0]


def test_qml_remove_row_removes_from_model() -> None:
QGuiApplication.instance() or QGuiApplication([])
model = EnvVarsModel()
model.reset_rows([("FOO", "1"), ("BAR", "2")])
modified: list[bool] = []
model.modified.connect(lambda: modified.append(True))

engine, root = _qml_root(model)
assert root.removeAt(0) is True
assert model.to_dict() == {"BAR": "2"}
assert modified, "removal must emit modified so the editor turns dirty"


def test_qml_remove_row_out_of_range_is_rejected() -> None:
QGuiApplication.instance() or QGuiApplication([])
model = EnvVarsModel()
model.reset_rows([("FOO", "1")])

engine, root = _qml_root(model)
assert root.removeAt(5) is False
assert root.removeAt(-1) is False
assert model.to_dict() == {"FOO": "1"}