-
Notifications
You must be signed in to change notification settings - Fork 543
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Python: Initial support for implementing models in Python
This provides a Model base class in Python and sub-classes of that can be set as data models in slint. The ListModel is provided as basic sub-class operating on a list() and allowing mutation and notifying the view on the Slint side. Similarly, an array declared in Slint is exposed as an object to Python that looks like a Model. Both support the sequence protocol. Fixes #4135
- Loading branch information
Showing
6 changed files
with
393 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
// Copyright © SixtyFPS GmbH <info@slint.dev> | ||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial | ||
|
||
use std::cell::RefCell; | ||
use std::rc::Rc; | ||
|
||
use i_slint_core::model::{Model, ModelNotify, ModelRc}; | ||
|
||
use pyo3::exceptions::PyIndexError; | ||
use pyo3::gc::PyVisit; | ||
use pyo3::prelude::*; | ||
use pyo3::PyTraverseError; | ||
|
||
use crate::value::PyValue; | ||
|
||
pub struct PyModelShared { | ||
notify: ModelNotify, | ||
self_ref: RefCell<Option<PyObject>>, | ||
} | ||
|
||
#[derive(Clone)] | ||
#[pyclass(unsendable, weakref, subclass)] | ||
pub struct PyModelBase { | ||
inner: Rc<PyModelShared>, | ||
} | ||
|
||
impl PyModelBase { | ||
pub fn as_model(&self) -> ModelRc<slint_interpreter::Value> { | ||
self.inner.clone().into() | ||
} | ||
} | ||
|
||
#[pymethods] | ||
impl PyModelBase { | ||
#[new] | ||
fn new() -> Self { | ||
Self { | ||
inner: Rc::new(PyModelShared { | ||
notify: Default::default(), | ||
self_ref: RefCell::new(None), | ||
}), | ||
} | ||
} | ||
|
||
fn init_self(&self, self_ref: PyObject) { | ||
*self.inner.self_ref.borrow_mut() = Some(self_ref); | ||
} | ||
|
||
fn notify_row_added(&self, index: usize, count: usize) { | ||
self.inner.notify.row_added(index, count) | ||
} | ||
|
||
fn notify_row_changed(&self, index: usize) { | ||
self.inner.notify.row_changed(index) | ||
} | ||
|
||
fn notify_row_removed(&self, index: usize, count: usize) { | ||
self.inner.notify.row_removed(index, count) | ||
} | ||
|
||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { | ||
if let Some(this) = self.inner.self_ref.borrow().as_ref() { | ||
visit.call(this)?; | ||
} | ||
Ok(()) | ||
} | ||
|
||
fn __clear__(&mut self) { | ||
*self.inner.self_ref.borrow_mut() = None; | ||
} | ||
} | ||
|
||
impl i_slint_core::model::Model for PyModelShared { | ||
type Data = slint_interpreter::Value; | ||
|
||
fn row_count(&self) -> usize { | ||
Python::with_gil(|py| { | ||
let obj = self.self_ref.borrow(); | ||
let Some(obj) = obj.as_ref() else { | ||
eprintln!("Python: Model implementation is lacking self object (in row_count)"); | ||
return 0; | ||
}; | ||
let result = match obj.call_method0(py, "row_count") { | ||
Ok(result) => result, | ||
Err(err) => { | ||
eprintln!( | ||
"Python: Model implementation of row_count() threw an exception: {}", | ||
err | ||
); | ||
return 0; | ||
} | ||
}; | ||
|
||
match result.extract::<usize>(py) { | ||
Ok(count) => count, | ||
Err(err) => { | ||
eprintln!("Python: Model implementation of row_count() returned value that cannot be cast to usize: {}", err); | ||
0 | ||
} | ||
} | ||
}) | ||
} | ||
|
||
fn row_data(&self, row: usize) -> Option<Self::Data> { | ||
Python::with_gil(|py| { | ||
let obj = self.self_ref.borrow(); | ||
let Some(obj) = obj.as_ref() else { | ||
eprintln!("Python: Model implementation is lacking self object (in row_data)"); | ||
return None; | ||
}; | ||
|
||
let result = match obj.call_method1(py, "row_data", (row,)) { | ||
Ok(result) => result, | ||
Err(err) if err.is_instance_of::<PyIndexError>(py) => return None, | ||
Err(err) => { | ||
eprintln!( | ||
"Python: Model implementation of row_data() threw an exception: {}", | ||
err | ||
); | ||
return None; | ||
} | ||
}; | ||
|
||
match result.extract::<PyValue>(py) { | ||
Ok(pv) => Some(pv.0), | ||
Err(err) => { | ||
eprintln!("Python: Model implementation of row_data() returned value that cannot be converted to Rust: {}", err); | ||
None | ||
} | ||
} | ||
}) | ||
} | ||
|
||
fn model_tracker(&self) -> &dyn i_slint_core::model::ModelTracker { | ||
&self.notify | ||
} | ||
|
||
fn as_any(&self) -> &dyn core::any::Any { | ||
self | ||
} | ||
} | ||
|
||
impl PyModelShared { | ||
pub fn rust_into_js_model(model: &ModelRc<slint_interpreter::Value>) -> Option<PyObject> { | ||
model | ||
.as_any() | ||
.downcast_ref::<PyModelShared>() | ||
.and_then(|rust_model| rust_model.self_ref.borrow().clone()) | ||
} | ||
} | ||
|
||
#[pyclass(unsendable)] | ||
pub struct ReadOnlyRustModel(pub ModelRc<slint_interpreter::Value>); | ||
|
||
#[pymethods] | ||
impl ReadOnlyRustModel { | ||
fn row_count(&self) -> usize { | ||
self.0.row_count() | ||
} | ||
|
||
fn row_data(&self, row: usize) -> Option<PyValue> { | ||
self.0.row_data(row).map(|value| value.into()) | ||
} | ||
|
||
fn __len__(&self) -> usize { | ||
self.row_count() | ||
} | ||
|
||
fn __iter__(slf: PyRef<'_, Self>) -> ReadOnlyRustModelIterator { | ||
ReadOnlyRustModelIterator { model: slf.0.clone(), row: 0 } | ||
} | ||
|
||
fn __getitem__(&self, index: usize) -> Option<PyValue> { | ||
self.row_data(index) | ||
} | ||
} | ||
|
||
impl From<&ModelRc<slint_interpreter::Value>> for ReadOnlyRustModel { | ||
fn from(model: &ModelRc<slint_interpreter::Value>) -> Self { | ||
Self(model.clone()) | ||
} | ||
} | ||
|
||
#[pyclass(unsendable)] | ||
struct ReadOnlyRustModelIterator { | ||
model: ModelRc<slint_interpreter::Value>, | ||
row: usize, | ||
} | ||
|
||
#[pymethods] | ||
impl ReadOnlyRustModelIterator { | ||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { | ||
slf | ||
} | ||
|
||
fn __next__(&mut self) -> Option<PyValue> { | ||
if self.row >= self.model.row_count() { | ||
return None; | ||
} | ||
let row = self.row; | ||
self.row += 1; | ||
self.model.row_data(row).map(|value| value.into()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# Copyright © SixtyFPS GmbH <info@slint.dev> | ||
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial | ||
|
||
from . import slint as native | ||
|
||
|
||
class Model(native.PyModelBase): | ||
def __new__(cls, *args): | ||
return super().__new__(cls) | ||
|
||
def __init__(self, lst=None): | ||
self.init_self(self) | ||
|
||
def __len__(self): | ||
return self.row_count() | ||
|
||
def __getitem__(self, index): | ||
return self.row_data(index) | ||
|
||
def __setitem__(self, index, value): | ||
self.set_row_data(index, value) | ||
|
||
def __iter__(self): | ||
return ModelIterator(self) | ||
|
||
|
||
class ListModel(Model): | ||
def __init__(self, lst=None): | ||
super().__init__() | ||
self.list = lst or [] | ||
|
||
def row_count(self): | ||
return len(self.list) | ||
|
||
def row_data(self, row): | ||
return self.list[row] | ||
|
||
def set_row_data(self, row, data): | ||
self.list[row] = data | ||
super().notify_row_changed(row) | ||
|
||
def __delitem__(self, key): | ||
if isinstance(key, slice): | ||
start, stop, step = key.indices(len(self.list)) | ||
del self.list[key] | ||
count = len(range(start, stop, step)) | ||
super().notify_row_removed(start, count) | ||
else: | ||
del self.list[key] | ||
super().notify_row_removed(key, 1) | ||
|
||
def append(self, value): | ||
index = len(self.list) | ||
self.list.append(value) | ||
super().notify_row_added(index, 1) | ||
|
||
|
||
class ModelIterator: | ||
def __init__(self, model): | ||
self.model = model | ||
self.index = 0 | ||
|
||
def __iter__(self): | ||
return self | ||
|
||
def __next__(self): | ||
if self.index >= self.model.row_count(): | ||
raise StopIteration() | ||
index = self.index | ||
self.index += 1 | ||
return self.model.row_data(index) |
Oops, something went wrong.