use tooltip widget to show subactions

This commit is contained in:
Jakub Trllo 2025-05-27 18:29:12 +02:00
parent 0be94fc2d8
commit d312101573
2 changed files with 267 additions and 102 deletions

View file

@ -829,6 +829,11 @@ HintedLineEditButton {
}
/* Launcher specific stylesheets */
ActionMenuToolTip {
border: 1px solid #555555;
background: {color:bg-inputs};
}
ActionVariantWidget {
background: transparent;
}

View file

@ -304,13 +304,219 @@ class ActionsQtModel(QtGui.QStandardItemModel):
self.refresh()
class ActionMenuToolTip(QtWidgets.QFrame):
def __init__(self, parent):
super().__init__(parent)
self.setWindowFlags(QtCore.Qt.ToolTip)
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating, True)
self.setAutoFillBackground(True)
self.setBackgroundRole(QtGui.QPalette.Base)
# Update size on show
show_timer = QtCore.QTimer()
show_timer.setSingleShot(True)
show_timer.setInterval(5)
# Close widget if is not updated by event
close_timer = QtCore.QTimer()
close_timer.setSingleShot(True)
close_timer.setInterval(100)
update_state_timer = QtCore.QTimer()
update_state_timer.setInterval(500)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
close_timer.timeout.connect(self.close)
show_timer.timeout.connect(self._on_show_timer)
update_state_timer.timeout.connect(self._on_update_state)
self._main_layout = main_layout
self._show_timer = show_timer
self._close_timer = close_timer
self._update_state_timer = update_state_timer
self._showed = False
self._mouse_entered = False
self._view_hovered = False
self._current_id = None
self._view = None
self._last_pos = QtCore.QPoint(0, 0)
self._widgets_by_id = {}
def showEvent(self, event):
self._showed = True
self._update_state_timer.start()
super().showEvent(event)
def closeEvent(self, event):
self._showed = False
self._update_state_timer.stop()
self._mouse_entered = False
super().closeEvent(event)
def enterEvent(self, event):
self._mouse_entered = True
self._close_timer.stop()
super().leaveEvent(event)
def leaveEvent(self, event):
self._mouse_entered = False
super().leaveEvent(event)
if not self._view_hovered:
self._close_timer.start()
def mouse_entered_view(self):
self._view_hovered = True
def mouse_left_view(self):
self._view_hovered = False
if not self._mouse_entered:
self._close_timer.start()
def show_on_event(self, action_id, action_items, view, event):
self._close_timer.stop()
self._view_hovered = True
is_current = action_id == self._current_id
if not is_current:
self._current_id = action_id
self._view = view
self._update_items(view, action_items)
# Nothing to show
if not self._widgets_by_id:
if self._showed:
self.close()
return
# Make sure is visible
update_position = not is_current
if not self._showed:
update_position = True
self.show()
self._last_pos = QtCore.QPoint(event.globalPos())
if not update_position:
# Only resize if is current
self.resize(self.sizeHint())
else:
# Set geometry to position
# - first make sure widget changes from '_update_items'
# are recalculated
app = QtWidgets.QApplication.instance()
app.processEvents()
self._on_update_state()
self.raise_()
self._show_timer.start()
def _on_show_timer(self):
size = self.sizeHint()
self.resize(size)
def _on_update_state(self):
if not self._view_hovered:
return
size = self.sizeHint()
pos = self._last_pos
offset = 4
self.setGeometry(
pos.x() + offset, pos.y() + offset,
size.width(), size.height()
)
def _update_items(self, view, action_items):
"""Update items in the tooltip."""
# This method can be used to update the content of the tooltip
# with new icon, text and settings button visibility.
remove_ids = set(self._widgets_by_id.keys())
new_ids = set()
widgets = []
any_has_settings = False
prepared_items = []
for idx, action_item in enumerate(action_items):
has_settings = bool(action_item.config_fields)
if has_settings:
any_has_settings = True
prepared_items.append((idx, action_item, has_settings))
if any_has_settings or len(action_items) > 1:
for idx, action_item, has_settings in prepared_items:
widget = self._widgets_by_id.get(action_item.identifier)
icon = get_qt_icon(action_item.icon)
label = action_item.full_label
if widget is None:
widget = ActionVariantWidget(
action_item.identifier, label, has_settings, self
)
widget.settings_requested.connect(
view.settings_requested
)
new_ids.add(action_item.identifier)
self._widgets_by_id[action_item.identifier] = widget
else:
remove_ids.discard(action_item.identifier)
widgets.append((idx, widget))
for action_id in remove_ids:
widget = self._widgets_by_id.pop(action_id)
widget.setVisible(False)
self._main_layout.removeWidget(widget)
widget.deleteLater()
for idx, widget in widgets:
self._main_layout.insertWidget(idx, widget, 0)
class ActionDelegate(QtWidgets.QStyledItemDelegate):
_cached_extender = {}
def __init__(self, *args, **kwargs):
super(ActionDelegate, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self._anim_start_color = QtGui.QColor(178, 255, 246)
self._anim_end_color = QtGui.QColor(5, 44, 50)
self._tooltip_widget = None
def helpEvent(self, event, view, option, index):
if not index.isValid():
if self._tooltip_widget is not None:
self._tooltip_widget.close()
return False
action_id = index.data(ACTION_ID_ROLE)
model = index.model()
source_model = model.sourceModel()
if index.data(ACTION_IS_GROUP_ROLE):
action_items = source_model.get_group_items(action_id)
else:
action_items = [source_model.get_action_item_by_id(action_id)]
if self._tooltip_widget is None:
self._tooltip_widget = ActionMenuToolTip(view)
self._tooltip_widget.show_on_event(
action_id, action_items, view, event
)
event.setAccepted(True)
return True
def close_tooltip(self):
if self._tooltip_widget is not None:
self._tooltip_widget.close()
def mouse_entered_view(self):
if self._tooltip_widget is not None:
self._tooltip_widget.mouse_entered_view()
def mouse_left_view(self):
if self._tooltip_widget is not None:
self._tooltip_widget.mouse_left_view()
def _draw_animation(self, painter, option, index):
grid_size = option.widget.gridSize()
@ -430,29 +636,51 @@ class ActionsProxyModel(QtCore.QSortFilterProxyModel):
return True
class ActionsView(QtWidgets.QListView):
settings_requested = QtCore.Signal(str)
def __init__(self, parent):
super().__init__(parent)
self.setProperty("mode", "icon")
self.setObjectName("IconView")
self.setViewMode(QtWidgets.QListView.IconMode)
self.setResizeMode(QtWidgets.QListView.Adjust)
self.setSelectionMode(QtWidgets.QListView.NoSelection)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.setWrapping(True)
self.setGridSize(QtCore.QSize(70, 75))
self.setIconSize(QtCore.QSize(30, 30))
self.setSpacing(0)
self.setWordWrap(True)
self.setToolTipDuration(150)
delegate = ActionDelegate(self)
self.setItemDelegate(delegate)
# Make view flickable
flick = FlickCharm(parent=self)
flick.activateOn(self)
self._flick = flick
self._delegate = delegate
def enterEvent(self, event):
super().enterEvent(event)
self._delegate.mouse_entered_view()
def leaveEvent(self, event):
super().leaveEvent(event)
self._delegate.mouse_left_view()
class ActionsWidget(QtWidgets.QWidget):
def __init__(self, controller, parent):
super(ActionsWidget, self).__init__(parent)
super().__init__(parent)
self._controller = controller
view = QtWidgets.QListView(self)
view.setProperty("mode", "icon")
view.setObjectName("IconView")
view.setViewMode(QtWidgets.QListView.IconMode)
view.setResizeMode(QtWidgets.QListView.Adjust)
view.setSelectionMode(QtWidgets.QListView.NoSelection)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
view.setWrapping(True)
view.setGridSize(QtCore.QSize(70, 75))
view.setIconSize(QtCore.QSize(30, 30))
view.setSpacing(0)
view.setWordWrap(True)
# Make view flickable
flick = FlickCharm(parent=view)
flick.activateOn(view)
view = ActionsView(self)
model = ActionsQtModel(controller)
@ -460,9 +688,6 @@ class ActionsWidget(QtWidgets.QWidget):
proxy_model.setSourceModel(model)
view.setModel(proxy_model)
delegate = ActionDelegate(self)
view.setItemDelegate(delegate)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
@ -472,13 +697,12 @@ class ActionsWidget(QtWidgets.QWidget):
animation_timer.timeout.connect(self._on_animation)
view.clicked.connect(self._on_clicked)
view.customContextMenuRequested.connect(self._on_context_menu)
view.settings_requested.connect(self._show_config_dialog)
model.refreshed.connect(self._on_model_refresh)
self._animated_items = set()
self._animation_timer = animation_timer
self._flick = flick
self._view = view
self._model = model
self._proxy_model = proxy_model
@ -572,37 +796,27 @@ class ActionsWidget(QtWidgets.QWidget):
return
is_group = index.data(ACTION_IS_GROUP_ROLE)
# TODO define and store what is default action for a group
action_id = index.data(ACTION_ID_ROLE)
project_name = self._model.get_selected_project_name()
folder_id = self._model.get_selected_folder_id()
task_id = self._model.get_selected_task_id()
if is_group:
action_item = self._show_menu_on_group(action_id)
if action_item is None:
return
action_id = action_item.identifier
action_label = action_item.full_label
action_type = action_item.action_type
addon_name = action_item.addon_name
addon_version = action_item.addon_version
else:
action_label = index.data(QtCore.Qt.DisplayRole)
action_type = index.data(ACTION_TYPE_ROLE)
addon_name = index.data(ACTION_ADDON_NAME_ROLE)
addon_version = index.data(ACTION_ADDON_VERSION_ROLE)
action_type = index.data(ACTION_TYPE_ROLE)
if action_type == "webaction":
action_item = self._model.get_action_item_by_id(action_id)
context = WebactionContext(
action_id,
project_name,
folder_id,
task_id,
addon_name,
addon_version
action_item.addon_name,
action_item.add_version
)
self._controller.trigger_webaction(
context, action_item.full_label
)
self._controller.trigger_webaction(context, action_label)
else:
self._controller.trigger_action(
action_id, project_name, folder_id, task_id
@ -610,57 +824,12 @@ class ActionsWidget(QtWidgets.QWidget):
self._start_animation(index)
def _show_menu_on_group(self, action_id):
action_items = self._model.get_group_items(action_id)
menu = QtWidgets.QMenu(self)
actions_mapping = {}
def on_settings_clicked(identifier):
# Close menu
menu.close()
# Show config dialog
self._show_config_dialog(identifier, False)
for action_item in action_items:
menu_action = ActionVariantAction(
action_item.identifier,
action_item.full_label,
bool(action_item.config_fields),
menu,
)
menu_action.settings_requested.connect(on_settings_clicked)
menu.addAction(menu_action)
actions_mapping[menu_action] = action_item
result = menu.exec_(QtGui.QCursor.pos())
if not result:
return None
return actions_mapping[result]
def _on_context_menu(self, point):
"""Creates menu to force skip opening last workfile."""
index = self._view.indexAt(point)
if not index.isValid():
return
action_id = index.data(ACTION_ID_ROLE)
if not action_id:
return
is_group = index.data(ACTION_IS_GROUP_ROLE)
self._show_config_dialog(action_id, is_group)
def _show_config_dialog(self, action_id, is_group):
item = self._model.get_item_by_id(action_id)
def _show_config_dialog(self, action_id):
action_item = self._model.get_action_item_by_id(action_id)
config_fields = self._model.get_action_config_fields(action_id)
if not config_fields:
return
addon_name = item.data(ACTION_ADDON_NAME_ROLE)
addon_version = item.data(ACTION_ADDON_VERSION_ROLE)
project_name = self._model.get_selected_project_name()
folder_id = self._model.get_selected_folder_id()
task_id = self._model.get_selected_task_id()
@ -669,8 +838,8 @@ class ActionsWidget(QtWidgets.QWidget):
project_name=project_name,
folder_id=folder_id,
task_id=task_id,
addon_name=addon_name,
addon_version=addon_version,
addon_name=action_item.addon_name,
addon_version=action_item.addon_version,
)
values = self._controller.get_action_config_values(context)
@ -682,17 +851,8 @@ class ActionsWidget(QtWidgets.QWidget):
)
dialog.set_values(values)
result = dialog.exec_()
if result != QtWidgets.QDialog.Accepted:
return
new_values = dialog.get_values()
if is_group:
action_items = self._model.get_group_items(action_id)
action_ids = [item.identifier for item in action_items]
else:
action_ids = [action_id]
for action_id in action_ids:
context.identifier = action_id
if result == QtWidgets.QDialog.Accepted:
new_values = dialog.get_values()
self._controller.set_action_config_values(context, new_values)
def _create_attrs_dialog(