whisker.qt

whisker/qt.py


Copyright © 2011-2020 Rudolf Cardinal (rudolf@pobox.com).

This file is part of the Whisker Python client library.

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.


Qt classes for Whisker GUI tasks.

class whisker.qt.DatabaseModelMixin(session: Session, listdata: List[Any], **kwargs)[source]

Mixin to provide functions that operate on items within a Qt Model and talk to an underlying SQLAlchemy database session.

Typically mixed in like this:

class MyListModel(QAbstractListModel, DatabaseModelMixin):
    pass
Parameters:
  • session – SQLAlchemy Session

  • listdata – data; a collection of SQLAlchemy ORM objects in a format that behaves like a list

  • kwargs – additional parameters for superclass (required for mixins)

delete_item(row_index: bool, delete_from_session: bool = True) None[source]

Deletes an item from the collection (and optionally from the SQLAlchemy session).

Parameters:
  • row_index – index

  • delete_from_session – also delete the ORM object from the SQLAlchemy session? Default is True.

Raises:

ValueError – if index invalid

get_object(index: int) Any[source]
Parameters:

index – integer index

Returns:

ORM object at the index, or None if the index is out of bounds

insert_at_index(obj: object, index: int | None = None, add_to_session: bool = True, flush: bool = True) None[source]

Inserts an ORM object into the data list.

Parameters:
  • obj – SQLAlchemy ORM object to insert

  • index – add it at this index location; specifying None adds it at the end of the list

  • add_to_session – also add the object to the SQLAlchemy session? Default is True.

  • flush – flush the SQLAlchemy session after adding the object? Only applicable if add_to_session is true.

Raises:

ValueError – if index invalid

item_deletable(rowindex: int) bool[source]

Override this if you need to prevent rows being deleted.

move_down(index: int) None[source]

Moves an object down one place in the list (from index to index + 1).

Parameters:

index – index of the object to move

Raises:

ValueError – if index invalid

move_up(index: int) None[source]

Moves an object up one place in the list (from index to index - 1).

Parameters:

index – index of the object to move

Raises:

ValueError – if index invalid

exception whisker.qt.EditCancelledException[source]

Exception to represent that the user cancelled editing.

class whisker.qt.GarbageCollector(parent: SimpleClass, debug: bool = False, interval_ms=10000)[source]

Disable automatic garbage collection and instead collect manually every INTERVAL milliseconds.

This is done to ensure that garbage collection only happens in the GUI thread, as otherwise Qt can crash.

See:

Parameters:
  • parent – parent QObject

  • debug – be verbose about garbage collection

  • interval_ms – period/interval (in ms) at which to perform garbage collection

check() None[source]

Check if garbage collection should happen, and if it should, do it.

static debug_cycles() None[source]

Collect garbage and print what was collected.

class whisker.qt.GenericAttrTableModel(data: ~typing.List[~typing.Any], header: ~typing.List[~typing.Tuple[str, str]], session: ~sqlalchemy.orm.session.Session, default_sort_column_name: str | None = None, default_sort_order: int = <MagicMock name='mock.AscendingOrder' id='139817544672544'>, deletable: bool = True, parent: SimpleClass | None = None, **kwargs)[source]

Takes a list of objects and a list of column headers; provides a view on it using func:str. That is, for a list of objects [a, b, c] and a list of headers ["h1", "h2", "h3"], it provides a table view like

h1

h2

h3

str(a.h1)

str(a.h2)

str(a.h3)

str(b.h1)

str(b.h2)

str(b.h3)

str(c.h1)

str(c.h2)

str(c.h3)

  • Note that it MODIFIES THE LIST PASSED TO IT.

  • Sorting: consider QSortFilterProxyModel… but not clear that can do arbitrary column sorts, since its sorting is via its lessThan() function.

  • The tricky part is keeping selections persistent after sorting. Not achieved yet. Simpler to wipe the selections.

Parameters:
  • data – data; a collection of SQLAlchemy ORM objects in a format that behaves like a list

  • header – list of (colname, attr_or_func) tuples. The first part, colname, is displayed to the user. The second part, attr_or_func, is used to retrieve data. For each object obj, the view will call thing = getattr(obj, attr_or_func). If thing is callable (i.e. is a member function), the view will display str(thing()); otherwise, it will display str(thing).

  • session – SQLAlchemy Session

  • default_sort_column_name – column to sort by to begin with, or None to respect the original order of data

  • default_sort_order – default sort order (default is ascending order)

  • deletable – may the user delete objects from the collection?

  • parent – parent (owning) QObject

  • kwargs – additional parameters for superclass (required for mixins)

columnCount(parent: <MagicMock id='139817549352480'> = <MagicMock name='mock()' id='139817544632016'>, *args, **kwargs)[source]

Qt override.

Parameters:
  • parent – parent object, specified by QModelIndex

  • args – unnecessary? Certainly unused.

  • kwargs – unnecessary? Certainly unused.

Returns:

number of columns

data(index: <MagicMock id='139817549352480'>, role: int = <MagicMock name='mock.DisplayRole' id='139817544656256'>) str | None[source]

Returns the data for a given row/column (in this case as a string).

Overrides QAbstractTableModel.data(); see http://doc.qt.io/qt-5/qabstractitemmodel.html.

Parameters:
  • indexQModelIndex, which specifies the row/column number

  • role – a way of specifying the type of view on the data; here, we only accept Qt.DisplayRole

Returns:

string representation of the relevant attribute (or member function result) for the relevant SQLAlchemy ORM object, or None

get_default_sort() Tuple[int, int][source]
Returns:

(default_sort_column_number, default_sort_order) where the column number is zero-based and the sort order is as per __init__()

headerData(col: int, orientation: int, role: int = <MagicMock name='mock.DisplayRole' id='139817544656256'>) str | None[source]

Qt override.

Returns the column heading for a particular column.

Parameters:
  • col – column number

  • orientation – required to be Qt.Horizontal

  • role – required to be Qt.DisplayRole

Returns:

column header string, or None

rowCount(parent: <MagicMock id='139817549352480'> = <MagicMock name='mock()' id='139817544632016'>, *args, **kwargs)[source]

Counts the number of rows.

Overrides QAbstractTableModel.rowCount(); see http://doc.qt.io/qt-5/qabstractitemmodel.html.

Parameters:
  • parent – parent object, specified by QModelIndex

  • args – unnecessary? Certainly unused.

  • kwargs – unnecessary? Certainly unused.

Returns:

“number of rows under the given parent. When the parent is valid… the number of children of parent.” Here: the number of objects in the list.

Return type:

In the Qt original

sort(col: int, order: int = <MagicMock name='mock.AscendingOrder' id='139817544672544'>) None[source]

Sort table on a specified column.

Parameters:
  • col – column number

  • order – sort order (e.g. ascending order)

class whisker.qt.GenericAttrTableView(session: Session, modal_dialog_class, parent: SimpleClass | None = None, sortable: bool = True, stretch_last_section: bool = True, readonly: bool = False, **kwargs)[source]

Provides a QTableView view on SQLAlchemy data.

Signals:

  • selection_changed(QItemSelection, QItemSelection)

Parameters:
  • session – SQLAlchemy Session

  • modal_dialog_class – class that is a subclass of TransactionalEditDialogMixin

  • parent – parent (owning) QObject

  • sortable – should we sort the data?

  • stretch_last_section – stretch the last column (section) horizontally?

  • readonly – read-only view?

  • kwargs – additional parameters for superclass (required for mixins)

get_selected_modelindex() <MagicMock id='139817549352480'> | None[source]

Gets the model index of the selected item.

Returns:

a QModelIndex or None

refresh_sizing() None[source]

Ask the view to resize its rows/columns to fit its data.

resize() None[source]

Resize all rows to have the correct height, and its columns to the correct width.

setModel(model: SimpleClass) None[source]

Qt override to set the model used by this view.

Parameters:

model – instance of the model

class whisker.qt.GenericListModel(data, session: Session, parent: SimpleClass | None = None, **kwargs)[source]

Takes a list and provides a view on it using str(). That is, for a list of object [a, b, c], it displays a list view with items (rows) like this:

str(a)

str(b)

str(c)

  • Note that it MODIFIES THE LIST PASSED TO IT.

data(index: <MagicMock id='139817549352480'>, role: int = <MagicMock name='mock.DisplayRole' id='139817544656256'>) str | None[source]

Returns the data for a given row (in this case as a string).

Overrides QAbstractListModel.data(); see http://doc.qt.io/qt-5/qabstractitemmodel.html.

Parameters:
  • indexQModelIndex, which specifies the row number

  • role – a way of specifying the type of view on the data; here, we only accept Qt.DisplayRole

Returns:

string representation of the object, or None

rowCount(parent: <MagicMock id='139817549352480'> = <MagicMock name='mock()' id='139817544632016'>, *args, **kwargs) int[source]

Counts the number of rows.

Overrides QAbstractListModel.rowCount(); see http://doc.qt.io/qt-5/qabstractitemmodel.html.

Parameters:
  • parent – parent object, specified by QModelIndex

  • args – unnecessary? Certainly unused.

  • kwargs – unnecessary? Certainly unused.

Returns:

“number of rows under the given parent. When the parent is valid… the number of children of parent.” Here: the number of objects in the list.

Return type:

In the Qt original

class whisker.qt.ModalEditListView(session: Session, modal_dialog_class: Type[TransactionalEditDialogMixin], *args, readonly: bool = False, **kwargs)[source]

A version of QListView that supports SQLAlchemy handling of its objects.

Parameters:
  • session – SQLAlchemy Session

  • modal_dialog_class – class that is a subclass of TransactionalEditDialogMixin

  • readonly – read-only view?

  • args – positional arguments to superclass

  • kwargs – keyword arguments to superclass

get_selected_modelindex() <MagicMock id='139817549352480'> | None[source]

Gets the model index of the selected item.

Returns:

a QModelIndex or None

setModel(model: SimpleClass) None[source]

Qt override to set the model used by this view.

Parameters:

model – instance of the model

class whisker.qt.RadioGroup(value_text_tuples: Iterable[Tuple[str, Any]], default: Any | None = None)[source]

Framework for radio buttons.

Parameters:
  • value_text_tuples – list of (value, text) tuples, where value is the value stored to Python object and text is the text shown to the user

  • default – default value

add_buttons_to_layout(layout: <MagicMock id='139817549505488'>) None[source]

Adds its button widgets to the specified Qt layout.

get_value() Any[source]
Returns:

the value of the currently selected radio button, or None if none is selected

set_value(value: Any) None[source]

Set the radio group to the specified value.

class whisker.qt.StatusMixin(name: str, logger: Logger, thread_info: bool = True, caller_info: bool = True, **kwargs)[source]

Add this to a QObject to provide easy Python logging and Qt signal-based status/error messaging.

It emits status messages to a Python log and to Qt signals.

Uses the same function names as Python logging, for predictability.

Signals:

  • status_sent(str, str)

  • error_sent(str, str)

Parameters:
  • name – name to add to log output

  • logger – Python logger to send output to

  • thread_info – show thread information as part of the output?

  • caller_info – add information about the function that sent the message?

  • kwargs – additional parameters for superclass (required for mixins)

class whisker.qt.TextLogElement(maximum_block_count: int = 1000, font_size_pt: int = 10, font_family: str = 'Courier', title: str = 'Log')[source]

Add a text log to your dialog box.

Encapsulates a QWidget representing a textual log in a group box.

Use this to embed a log within another Qt dialogue.

(This is nothing to do with the Python logging logs.)

Parameters:
  • maximum_block_count – the maximum number of blocks (log entries) to show in this window

  • font_size_pt – font size, in points

  • font_family – font family

  • title – title for group box

add(msg: str) None[source]

Adds a message to the log.

copy_whole_log() None[source]

Copies the log to the clipboard.

get_widget() <MagicMock id='139817549147296'>[source]
Returns:

a QWidget that is the group box containing the log and its controls

scroll_to_end_of_log() None[source]

Scrolls the log so that the last entry is visible.

class whisker.qt.TransactionalEditDialogMixin(session: ~sqlalchemy.orm.session.Session, obj: object, layout: <MagicMock id='139817549505488'>, readonly: bool = False, **kwargs)[source]

Mixin for a config-editing dialogue. Use it as:

class MyEditingDialog(TransactionalEditDialogMixin, QDialog):
    pass

Wraps the editing in a SAVEPOINT transaction. The caller must still commit() afterwards, but any rollbacks are automatic.

See also http://pyqt.sourceforge.net/Docs/PyQt5/multiinheritance.html for the super().__init__(..., **kwargs) chain.

Signals:

  • ok()

Parameters:
  • session – SQLAlchemy Session

  • obj – SQLAlchemy ORM object being edited

  • layout – Qt class`QLayout` to encapsulate within the class:QDialog’s main layout. This should contain all the editing widgets. The mixin wraps that with OK/cancel buttons.

  • readonly – make this a read-only view

  • kwargs – additional parameters to superclass

dialog_to_object(obj: object) None[source]

The class using the mixin must override this to write information from the dialogue box to the SQLAlchemy ORM object. It should raise an exception if validation fails. (This class isn’t fussy about which exception that is; it checks for Exception.)

edit_in_nested_transaction() int[source]

Pops up the dialog, allowing editing.

  • Does so within a database transaction.

  • If the user clicks OK and the data validates, commits the transaction.

  • If the user cancels, rolls back the transaction.

  • We want it nestable, so that the config dialog box can edit part of the config, reversibly, without too much faffing around.

Development notes:

class whisker.qt.ViewAssistMixin(session: Session, modal_dialog_class: Type[TransactionalEditDialogMixin], readonly: bool = False, *args, **kwargs)[source]

Mixin to add SQLAlchemy database-handling functions to a ViewAssistMixin.

Typically used like this:

class MyListView(QListView, ViewAssistMixin):
    pass

Signals:

  • selection_changed(QItemSelection, QItemSelection)

  • selected_maydelete(bool, bool)

Parameters:
  • session – SQLAlchemy Session

  • modal_dialog_class – class that is a subclass of TransactionalEditDialogMixin

  • readonly – read-only view?

  • kwargs – additional parameters for superclass (required for mixins)

add_in_nested_transaction(new_object: object, at_index: int | None = None) int | None[source]

Starts an SQLAlchemy nested transaction (which uses SAVEPOINT SQL); adds the new object into the session; edits the object. If the editing is cancelled, cancel the addition. Otherwise, the object is added to the session (in a nested transaction that might be cancelled by a caller) and to the model/list.

Parameters:
  • new_object – SQLAlchemy ORM object to insert

  • at_index – add it at this index location; specifying None (the default) adds it at the end of the list; 0 is the start

clear_selection() None[source]

Clears any selection.

edit(index: <MagicMock id='139817549352480'>, trigger: <MagicMock name='mock.EditTrigger' id='139817544607632'>, event: <MagicMock id='139817549404816'>) bool[source]

Edits the specified object.

Overrides QAbstractItemView::edit(); see http://doc.qt.io/qt-5/qabstractitemview.html#edit-1.

Parameters:
  • indexQModelIndex of the object to edit

  • trigger – action that caused the editing process

  • event – associated QEvent

Returns:

True if the view’s State is now EditingState; otherwise False

edit_by_modelindex(index: <MagicMock id='139817549352480'>, readonly: bool | None = None) None[source]

Edits the specified object.

Parameters:
  • indexQModelIndex of the object to edit

  • readonly – read-only view? If None (the default), uses the setting from self.

edit_selected(readonly: bool | None = None) None[source]

Edits the currently selected object.

Parameters:

readonly – read-only view? If None (the default), uses the setting from self.

get_n_rows() int[source]
Returns:

the number of rows (items) present

get_selected_modelindex() <MagicMock id='139817549352480'> | None[source]

Returns the QModelIndex of the currently selected row, or None.

Should be overridden in derived classes.

get_selected_object() object | None[source]

Returns the SQLAlchemy ORM object that’s currently selected, or None.

get_selected_row_index() int | None[source]

Gets the index of the currently selected row.

Returns:

index, or None

go_to(row: int | None) None[source]

Makes a specific row (by index) the currently selected row.

Parameters:

row – row index, or None to select the last row if there is one

insert_at_end(obj: object, add_to_session: bool = True, flush: bool = True) None[source]

Insert a SQLAlchemy ORM object into the model at the end of the list (and, optionally, into the SQLAlchemy session).

Parameters:
  • obj – SQLAlchemy ORM object to insert

  • add_to_session – also add the object to the SQLAlchemy session? Default is True.

  • flush – flush the SQLAlchemy session after adding the object? Only applicable if add_to_session is true.

insert_at_index(obj: object, index: int | None = None, add_to_session: bool = True, flush: bool = True) None[source]

Insert an SQLAlchemy ORM object into the model at a specific location (and, optionally, into the SQLAlchemy session).

Parameters:
  • obj – SQLAlchemy ORM object to insert

  • index – index to insert at; None for end, 0 for start

  • add_to_session – also add the object to the SQLAlchemy session? Default is True.

  • flush – flush the SQLAlchemy session after adding the object? Only applicable if add_to_session is true.

insert_at_start(obj: object, add_to_session: bool = True, flush: bool = True) None[source]

Insert a SQLAlchemy ORM object into the model at the start of the list (and, optionally, into the SQLAlchemy session).

Parameters:
  • obj – SQLAlchemy ORM object to insert

  • add_to_session – also add the object to the SQLAlchemy session? Default is True.

  • flush – flush the SQLAlchemy session after adding the object? Only applicable if add_to_session is true.

is_selected() bool[source]

Is a row currently selected?

move_selected_down() None[source]

Moves the selected object down one place in the list.

move_selected_up() None[source]

Moves the selected object up one place in the list.

remove_by_index(row_index: int, delete_from_session: bool = True) None[source]

Remove a specified SQLAlchemy ORM object from the list.

Parameters:
  • row_index – index of object to remove

  • delete_from_session – also delete it from the SQLAlchemy session? (default: True)

remove_selected(delete_from_session: bool = True) None[source]

Remove the currently selected SQLAlchemy ORM object from the list.

Parameters:

delete_from_session – also delete it from the SQLAlchemy session? (default: True)

set_model_common(model: SimpleClass, listview_base_class: Type[SimpleClass]) None[source]

Helper function to set up a Qt model.

Parameters:
  • model – instance of the Qt model

  • listview_base_class – class being used as the Qt view

whisker.qt.exit_on_exception(func)[source]

Decorator to stop whole program on exceptions (use for threaded slots).

See

Parameters:

func – the function to decorate

Returns:

the decorated function

whisker.qt.run_gui(qt_app: <MagicMock id='139817549465920'>, win: <MagicMock id='139817549524608'>) int[source]

Run a GUI, given a base window.

Parameters:
  • qt_app – Qt application

  • win – Qt dialogue window

Returns:

exit code