Skip to content

Commit

Permalink
Refactor database names/DB editor window and tab management (#2997)
Browse files Browse the repository at this point in the history
  • Loading branch information
soininen authored Nov 1, 2024
2 parents 18814fa + 651ed29 commit 59a0d4e
Show file tree
Hide file tree
Showing 62 changed files with 824 additions and 458 deletions.
120 changes: 120 additions & 0 deletions spinetoolbox/database_display_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# 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/>.
######################################################################################################################

"""This module contains functionality to manage database display names."""
import hashlib
import pathlib
from PySide6.QtCore import QObject, Signal, Slot
from sqlalchemy.engine.url import URL, make_url


class NameRegistry(QObject):
display_name_changed = Signal(str, str)
"""Emitted when the display name of a database changes."""

def __init__(self, parent=None):
"""
Args:
parent (QObject, optional): parent object
"""
super().__init__(parent)
self._names_by_url: dict[str, set[str]] = {}

@Slot(str, str)
def register(self, db_url, name):
"""Registers a new name for given database URL.
Args:
db_url (URL or str): database URL
name (str): name to register
"""
url = str(db_url)
if url in self._names_by_url and name in self._names_by_url[url]:
return
self._names_by_url.setdefault(url, set()).add(name)
self.display_name_changed.emit(url, self.display_name(db_url))

@Slot(str, str)
def unregister(self, db_url, name):
"""Removes a name from the registry.
Args:
db_url (URL or str): database URL
name (str): name to remove
"""
url = str(db_url)
names = self._names_by_url[url]
old_name = self.display_name(url) if len(names) in (1, 2) else None
names.remove(name)
if old_name is not None:
new_name = self.display_name(url)
self.display_name_changed.emit(url, new_name)

def display_name(self, db_url):
"""Makes display name for a database.
Args:
db_url (URL or str): database URL
Returns:
str: display name
"""
try:
registered_names = self._names_by_url[str(db_url)]
except KeyError:
return suggest_display_name(db_url)
else:
if len(registered_names) == 1:
return next(iter(registered_names))
return suggest_display_name(db_url)

def display_name_iter(self, db_maps):
"""Yields database mapping display names.
Args:
db_maps (Iterable of DatabaseMapping): database mappings
Yields:
str: display name
"""
yield from (self.display_name(db_map.sa_url) for db_map in db_maps)

def map_display_names_to_db_maps(self, db_maps):
"""Returns a dictionary that maps display names to database mappings.
Args:
db_maps (Iterable of DatabaseMapping): database mappings
Returns:
dict: database mappings keyed by display names
"""
return {self.display_name(db_map.sa_url): db_map for db_map in db_maps}


def suggest_display_name(db_url):
"""Returns a short name for the database mapping.
Args:
db_url (URL or str): database URL
Returns:
str: suggested name for the database for display purposes.
"""
if not isinstance(db_url, URL):
db_url = make_url(db_url)
if not db_url.drivername.startswith("sqlite"):
return db_url.database
if db_url.database is not None:
return pathlib.Path(db_url.database).stem
hashing = hashlib.sha1()
hashing.update(bytes(str(id(db_url)), "utf-8"))
return hashing.hexdigest()
71 changes: 71 additions & 0 deletions spinetoolbox/multi_tab_windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# 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/>.
######################################################################################################################

"""Contains functionality to keep track on open MultiTabWindow instances."""
from spinetoolbox.widgets.multi_tab_window import MultiTabWindow


class MultiTabWindowRegistry:
"""Registry that holds multi tab windows."""

def __init__(self):
self._multi_tab_windows: list[MultiTabWindow] = []

def has_windows(self):
"""Tests if there are any windows registered.
Returns:
bool: True if editor windows exist, False otherwise
"""
return bool(self._multi_tab_windows)

def windows(self):
"""Returns a list of multi tab windows.
Returns:
list of MultiTabWindow: windows
"""
return list(self._multi_tab_windows)

def tabs(self):
"""Returns a list of tabs across all windows.
Returns:
list of QWidget: tab widgets
"""
return [
window.tab_widget.widget(k) for window in self._multi_tab_windows for k in range(window.tab_widget.count())
]

def register_window(self, window):
"""Registers a new multi tab window.
Args:
window (MultiTabWindow): window to register
"""
self._multi_tab_windows.append(window)

def unregister_window(self, window):
"""Removes multi tab window from the registry.
Args:
window (MultiTabWindow): window to unregister
"""
self._multi_tab_windows.remove(window)

def get_some_window(self):
"""Returns a random multi tab window or None if none is available.
Returns:
MultiTabWindow: editor window
"""
return self._multi_tab_windows[0] if self._multi_tab_windows else None
7 changes: 4 additions & 3 deletions spinetoolbox/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,12 +692,13 @@ def plot_pivot_table_selection(model, model_indexes, plot_widget=None):
return plot_data(data_list, plot_widget)


def plot_db_mngr_items(items, db_maps, plot_widget=None):
def plot_db_mngr_items(items, db_maps, db_name_registry, plot_widget=None):
"""Returns a plot widget with plots of database manager parameter value items.
Args:
items (list of dict): parameter value items
db_maps (list of DatabaseMapping): database mappings corresponding to items
db_name_registry (NameRegistry): database display name registry
plot_widget (PlotWidget, optional): widget to add plots to
"""
if not items:
Expand All @@ -707,13 +708,13 @@ def plot_db_mngr_items(items, db_maps, plot_widget=None):
root_node = TreeNode("database")
for item, db_map in zip(items, db_maps):
value = from_database(item["value"], item["type"])
db_name = db_name_registry.display_name(db_map.sa_url)
if value is None:
continue
try:
leaf_content = _convert_to_leaf(value)
except PlottingError as error:
raise PlottingError(f"Failed to plot value in {db_map.codename}: {error}") from error
db_name = db_map.codename
raise PlottingError(f"Failed to plot value in {db_name}: {error}") from error
parameter_name = item["parameter_definition_name"]
entity_byname = item["entity_byname"]
if not isinstance(entity_byname, tuple):
Expand Down
8 changes: 4 additions & 4 deletions spinetoolbox/spine_db_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs):
self.redo_data = data
self.undo_ids = None
self._check = check
self.setText(f"add {item_type} items to {db_map.codename}")
self.setText(f"add {item_type} items to {db_mngr.name_registry.display_name(db_map.sa_url)}")

def redo(self):
super().redo()
Expand Down Expand Up @@ -143,7 +143,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs):
if self.redo_data == self.undo_data:
self.setObsolete(True)
self._check = check
self.setText(f"update {item_type} items in {db_map.codename}")
self.setText(f"update {item_type} items in {self.db_mngr.name_registry.display_name(db_map.sa_url)}")

def redo(self):
super().redo()
Expand Down Expand Up @@ -183,7 +183,7 @@ def __init__(self, db_mngr, db_map, item_type, data, check=True, **kwargs):
self.redo_update_data = None
self.undo_remove_ids = None
self.undo_update_data = None
self.setText(f"update {item_type} items in {db_map.codename}")
self.setText(f"update {item_type} items in {self.db_mngr.name_registry.display_name(db_map.sa_url)}")

def redo(self):
super().redo()
Expand Down Expand Up @@ -225,7 +225,7 @@ def __init__(self, db_mngr, db_map, item_type, ids, check=True, **kwargs):
self.item_type = item_type
self.ids = ids
self._check = check
self.setText(f"remove {item_type} items from {db_map.codename}")
self.setText(f"remove {item_type} items from {self.db_mngr.name_registry.display_name(db_map.sa_url)}")

def redo(self):
super().redo()
Expand Down
16 changes: 16 additions & 0 deletions spinetoolbox/spine_db_editor/editors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# 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/>.
######################################################################################################################

"""Contains Spine Database editor's window registry."""
from spinetoolbox.multi_tab_windows import MultiTabWindowRegistry

db_editor_registry = MultiTabWindowRegistry()
12 changes: 8 additions & 4 deletions spinetoolbox/spine_db_editor/graphics_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def display_data(self):

@property
def display_database(self):
return ",".join([db_map.codename for db_map in self.db_maps])
return ", ".join(self.db_mngr.name_registry.display_name_iter(self.db_maps))

@property
def db_maps(self):
Expand Down Expand Up @@ -370,7 +370,7 @@ def default_parameter_data(self):
return {
"entity_class_name": self.entity_class_name,
"entity_byname": DB_ITEM_SEPARATOR.join(self.byname),
"database": self.first_db_map.codename,
"database": self.db_mngr.name_registry.display_name(self.first_db_map.sa_url),
}

def shape(self):
Expand Down Expand Up @@ -667,7 +667,7 @@ def _populate_connect_entities_menu(self, menu):
for name, db_map_ent_clss in self._db_map_entity_class_lists.items():
for db_map, ent_cls in db_map_ent_clss:
icon = self.db_mngr.entity_class_icon(db_map, ent_cls["id"])
action_name = name + "@" + db_map.codename
action_name = name + "@" + self.db_mngr.name_registry.display_name(db_map.sa_url)
enabled = set(ent_cls["dimension_id_list"]) <= entity_class_ids_in_graph.get(db_map, set())
action_name_icon_enabled.append((action_name, icon, enabled))
for action_name, icon, enabled in sorted(action_name_icon_enabled):
Expand Down Expand Up @@ -702,7 +702,11 @@ def _start_connecting_entities(self, action):
class_name, db_name = action.text().split("@")
db_map_ent_cls_lst = self._db_map_entity_class_lists[class_name]
db_map, ent_cls = next(
iter((db_map, ent_cls) for db_map, ent_cls in db_map_ent_cls_lst if db_map.codename == db_name)
iter(
(db_map, ent_cls)
for db_map, ent_cls in db_map_ent_cls_lst
if self.db_mngr.name_registry.display_name(db_map.sa_url) == db_name
)
)
self._spine_db_editor.start_connecting_entities(db_map, ent_cls, self)

Expand Down
4 changes: 2 additions & 2 deletions spinetoolbox/spine_db_editor/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ def main():
editor = MultiSpineDBEditor(db_mngr)
if args.separate_tabs:
for url in args.url:
editor.add_new_tab({url: None})
editor.add_new_tab([url])
else:
editor.add_new_tab({url: None for url in args.url})
editor.add_new_tab(args.url)
editor.show()
return_code = app.exec()
return return_code
Expand Down
13 changes: 2 additions & 11 deletions spinetoolbox/spine_db_editor/mvcmodels/alternative_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@
class DBItem(EmptyChildMixin, FetchMoreMixin, StandardDBItem):
"""A root item representing a db."""

@property
def item_type(self):
return "db"

@property
def fetch_item_type(self):
return "alternative"
Expand All @@ -38,13 +34,8 @@ def _make_child(self, id_):
class AlternativeItem(GrayIfLastMixin, EditableMixin, LeafItem):
"""An alternative leaf item."""

@property
def item_type(self):
return "alternative"

@property
def icon_code(self):
return _ALTERNATIVE_ICON
item_type = "alternative"
icon_code = _ALTERNATIVE_ICON

def tool_tip(self, column):
if column == 0 and self.id:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class AlternativeModel(TreeModelBase):
"""A model to display alternatives in a tree view."""

def _make_db_item(self, db_map):
return DBItem(self, db_map)
return DBItem(self, db_map, self.db_mngr.name_registry)

def mimeData(self, indexes):
"""Stores selected indexes into MIME data.
Expand Down
Loading

0 comments on commit 59a0d4e

Please sign in to comment.