Source code for spinetoolbox.spine_db_editor.widgets.custom_menus
######################################################################################################################
# Copyright (C) 2017-2021 Spine project consortium
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################
"""
Classes for custom context menus and pop-up menus.
:author: M. Marin (KTH)
:date: 13.5.2020
"""
from PySide2.QtWidgets import QWidgetAction, QMenu, QWidget
from PySide2.QtCore import Qt, QEvent, QPoint, Signal, Slot
from PySide2.QtGui import QKeyEvent, QKeySequence
from .custom_qwidgets import LazyFilterWidget, DataToValueFilterWidget
from ...widgets.custom_menus import FilterMenuBase
[docs]class MainMenu(QMenu):
[docs] def event(self, ev):
"""Intercepts shortcurts and instead sends an equivalent event with the 'Alt' modifier,
so that mnemonics works with just the key.
Also sends a key press event with the 'Alt' key when this menu shows,
so that mnemonics are underligned on windows.
"""
if ev.type() == QEvent.KeyPress and ev.key() == Qt.Key_Alt:
return True
if ev.type() == QEvent.ShortcutOverride and ev.modifiers() == Qt.NoModifier:
actions = self.actions() + [a for child in self.findChildren(QWidget) for a in child.actions()]
mnemonics = [QKeySequence.mnemonic(a.text()) for a in actions]
key_seq = QKeySequence(Qt.ALT + ev.key())
if key_seq in mnemonics:
ev = QKeyEvent(QEvent.KeyPress, ev.key(), Qt.AltModifier)
qApp.postEvent(self, ev) # pylint: disable=undefined-variable
return True
if ev.type() == QEvent.Show:
pev = QKeyEvent(QEvent.KeyPress, Qt.Key_Alt, Qt.NoModifier)
qApp.postEvent(self, pev) # pylint: disable=undefined-variable
return super().event(ev)
[docs]class ParameterViewFilterMenu(FilterMenuBase):
def __init__(self, parent, source_model, field, show_empty=True):
"""
Args:
parent (SpineDBEditor)
source_model (CompoundParameterModel): a model to lazily get data from
field (str): the field name
"""
super().__init__(parent)
self._source_model = source_model
self._field = field
self._filter = LazyFilterWidget(self, source_model, show_empty=show_empty)
self._filter_action = QWidgetAction(parent)
self._filter_action.setDefaultWidget(self._filter)
self.addAction(self._filter_action)
self._menu_data = dict() # Maps value to set of (db map, entity_class id, item id)
self._inv_menu_data = dict() # Maps tuple (db map, entity_class id, item id) to value
self.connect_signals()
self.aboutToShow.connect(self._filter.set_model)
self._source_model.refreshed.connect(self._handle_source_model_refreshed)
@Slot()
[docs] def _handle_source_model_refreshed(self):
"""Updates the menu to only present values that are actually shown in the source model."""
accepted_identifiers = {(m.db_map, m.entity_class_id, m.item_id(i)) for m, i in self._source_model._row_map}
accepted_values = {
value for value, identifiers in self._menu_data.items() if identifiers.intersection(accepted_identifiers)
}
self.set_filter_accepted_values(accepted_values)
[docs] def set_filter_accepted_values(self, accepted_values):
self._filter._filter_model.set_base_filter(lambda x: x in accepted_values)
[docs] def set_filter_rejected_values(self, rejected_values):
self._filter._filter_model.set_base_filter(lambda x: x not in rejected_values)
[docs] def _get_value_to_remove(self, action, db_map, db_item):
if action not in ("remove", "update"):
return None
entity_class_id = db_item.get(self._source_model.entity_class_id_key)
item_id = db_item["id"]
identifier = (db_map, entity_class_id, item_id)
old_value = self._inv_menu_data.pop(identifier, None)
if old_value is None:
return None
old_items = self._menu_data[old_value]
old_items.remove(identifier)
if not old_items:
del self._menu_data[old_value]
return old_value
[docs] def _get_value_to_add(self, action, db_map, db_item):
if action not in ("add", "update"):
return None
entity_class_id = db_item.get(self._source_model.entity_class_id_key)
item_id = db_item["id"]
identifier = (db_map, entity_class_id, item_id)
value = db_map.codename if self._field == "database" else db_item[self._field]
self._inv_menu_data[identifier] = value
if value not in self._menu_data:
self._menu_data[value] = {identifier}
return value
self._menu_data[value].add(identifier)
[docs] def _build_auto_filter(self, valid_values):
"""
Builds the auto filter given valid values.
Args:
valid_values (Sequence): Values accepted by the filter.
Returns:
dict: mapping db_map, to entity_class_id, to set of accepted parameter_value/definition ids
"""
if not self._filter.has_filter():
return {} # All-pass
if not valid_values:
return None # You shall not pass
auto_filter = {}
for value in valid_values:
for db_map, entity_class_id, item_id in self._menu_data[value]:
auto_filter.setdefault(db_map, {}).setdefault(entity_class_id, set()).add(item_id)
return auto_filter
[docs] def emit_filter_changed(self, valid_values):
"""
Builds auto filter and emits signal.
Args:
valid_values (Sequence): Values accepted by the filter.
"""
auto_filter = self._build_auto_filter(valid_values)
self.filterChanged.emit(self._field, auto_filter)
[docs]class TabularViewFilterMenu(FilterMenuBase):
"""Filter menu to use together with FilterWidget in TabularViewMixin."""
def __init__(self, parent, identifier, data_to_value, show_empty=True):
"""
Args:
parent (SpineDBEditor)
identifier (int): index identifier
data_to_value (method): a method to translate item data to a value for display role
"""
super().__init__(parent)
self.identifier = identifier
self._filter = DataToValueFilterWidget(self, data_to_value, show_empty=show_empty)
self._filter_action = QWidgetAction(parent)
self._filter_action.setDefaultWidget(self._filter)
self.addAction(self._filter_action)
self.anchor = parent
self.connect_signals()
[docs] def emit_filter_changed(self, valid_values):
self.filterChanged.emit(self.identifier, valid_values, self._filter.has_filter())
[docs] def event(self, event):
if event.type() == QEvent.Show and self.anchor is not None:
if self.anchor.area == "rows":
pos = self.anchor.mapToGlobal(QPoint(0, 0)) + QPoint(0, self.anchor.height())
elif self.anchor.area == "columns":
pos = self.anchor.mapToGlobal(QPoint(0, 0)) + QPoint(self.anchor.width(), 0)
self.move(pos)
return super().event(event)