diff --git a/src/main/python/plotlyst/core/domain.py b/src/main/python/plotlyst/core/domain.py index 064fe6fe9..7f37d785c 100644 --- a/src/main/python/plotlyst/core/domain.py +++ b/src/main/python/plotlyst/core/domain.py @@ -972,6 +972,7 @@ class WorldBuildingEntityType(Enum): SETTING = 2 GROUP = 3 ITEM = 4 + CONTAINER = 5 @dataclass @@ -1477,7 +1478,7 @@ class ImportOrigin: class NovelDescriptor: title: str id: uuid.UUID = field(default_factory=uuid.uuid4) - lang_settings: LanguageSettings = LanguageSettings() + lang_settings: LanguageSettings = field(default_factory=LanguageSettings) import_origin: Optional[ImportOrigin] = None subtitle: str = field(default='', metadata=config(exclude=exclude_if_empty)) icon: str = field(default='', metadata=config(exclude=exclude_if_empty)) @@ -1782,8 +1783,8 @@ class Novel(NovelDescriptor): premise: str = '' synopsis: Optional['Document'] = None prefs: NovelPreferences = field(default_factory=NovelPreferences) - world: WorldBuilding = WorldBuilding() - board: Board = Board() + world: WorldBuilding = field(default_factory=WorldBuilding) + board: Board = field(default_factory=Board) def pov_characters(self) -> List[Character]: pov_ids = set() diff --git a/src/main/python/plotlyst/test/view/test_scenes.py b/src/main/python/plotlyst/test/view/test_scenes.py index f9a4ec2c5..e2ef7d6b5 100644 --- a/src/main/python/plotlyst/test/view/test_scenes.py +++ b/src/main/python/plotlyst/test/view/test_scenes.py @@ -2,7 +2,7 @@ from PyQt6.QtCharts import QPieSeries from PyQt6.QtCore import Qt, QModelIndex -from PyQt6.QtGui import QBrush, QColor, QAction +from PyQt6.QtGui import QAction from PyQt6.QtWidgets import QMessageBox, QSpinBox from src.main.python.plotlyst.core.client import client @@ -194,7 +194,7 @@ def _edit_day(editor: QSpinBox): def test_character_distribution_display(qtbot, filled_window: MainWindow): def assert_painted(index: QModelIndex): - assert index.data(role=Qt.ItemDataRole.BackgroundRole) == QBrush(QColor('darkblue')) + assert index.data(role=Qt.ItemDataRole.BackgroundRole) is not None def assert_not_painted(index: QModelIndex): assert index.data(role=Qt.ItemDataRole.BackgroundRole) is None diff --git a/src/main/python/plotlyst/view/icons.py b/src/main/python/plotlyst/view/icons.py index f172b7b84..22876f81c 100644 --- a/src/main/python/plotlyst/view/icons.py +++ b/src/main/python/plotlyst/view/icons.py @@ -612,6 +612,10 @@ def big_five_icon(color_on: str = '#7209b7') -> QIcon: def expand_icon() -> QIcon: return IconRegistry.from_name('fa5s.expand-alt', vflip=True) + @staticmethod + def group_icon() -> QIcon: + return IconRegistry.from_name('mdi.account-group') + @staticmethod def docx_icon() -> QIcon: return IconRegistry.from_name('mdi.file-word-outline') diff --git a/src/main/python/plotlyst/view/main_window.py b/src/main/python/plotlyst/view/main_window.py index 764a100d8..d326e31b3 100644 --- a/src/main/python/plotlyst/view/main_window.py +++ b/src/main/python/plotlyst/view/main_window.py @@ -131,7 +131,6 @@ def __init__(self, *args, **kwargs): self.btnScenes.setIcon(IconRegistry.scene_icon(NAV_BAR_BUTTON_DEFAULT_COLOR, NAV_BAR_BUTTON_CHECKED_COLOR)) self.btnWorld.setIcon( IconRegistry.world_building_icon(NAV_BAR_BUTTON_DEFAULT_COLOR, NAV_BAR_BUTTON_CHECKED_COLOR)) - self.btnWorld.setHidden(True) self.btnNotes.setIcon( IconRegistry.document_edition_icon(NAV_BAR_BUTTON_DEFAULT_COLOR, NAV_BAR_BUTTON_CHECKED_COLOR)) self.btnManuscript.setIcon( diff --git a/src/main/python/plotlyst/view/manuscript_view.py b/src/main/python/plotlyst/view/manuscript_view.py index e5c793ec3..242703108 100644 --- a/src/main/python/plotlyst/view/manuscript_view.py +++ b/src/main/python/plotlyst/view/manuscript_view.py @@ -58,7 +58,7 @@ def __init__(self, novel: Novel): super().__init__(novel, [NovelUpdatedEvent, SceneChangedEvent, ChapterChangedEvent, SceneDeletedEvent]) self.ui = Ui_ManuscriptView() self.ui.setupUi(self.widget) - self.ui.splitter.setSizes([100, 500]) + self.ui.splitter.setSizes([150, 500]) self.ui.splitterEditor.setSizes([400, 150]) self.ui.stackedWidget.setCurrentWidget(self.ui.pageOverview) diff --git a/src/main/python/plotlyst/view/style/base.py b/src/main/python/plotlyst/view/style/base.py index e6d1a7f5a..485559fbb 100644 --- a/src/main/python/plotlyst/view/style/base.py +++ b/src/main/python/plotlyst/view/style/base.py @@ -29,6 +29,7 @@ style = ''' * { icon-size: 20px; + color: #040406; } QToolTip { diff --git a/src/main/python/plotlyst/view/style/tab.py b/src/main/python/plotlyst/view/style/tab.py index f32686249..221866bcc 100644 --- a/src/main/python/plotlyst/view/style/tab.py +++ b/src/main/python/plotlyst/view/style/tab.py @@ -19,29 +19,34 @@ """ style = ''' -QTabWidget::pane { - border: 1px solid black; - background: #f8f9fa; + +QTabWidget::pane[borderless=true] { + border-top: 1px solid lightgrey; } -QTabWidget::tab-bar:top { +QTabWidget::tab-bar:top[centered=false] { top: 1px; } -QTabWidget::tab-bar:bottom { +QTabWidget::tab-bar:bottom[centered=false] { bottom: 1px; } -QTabWidget::tab-bar:left { +QTabWidget::tab-bar:left[centered=false] { right: 1px; } -QTabWidget::tab-bar:right { +QTabWidget::tab-bar:right[centered=false] { left: 1px; } +QTabWidget::tab-bar[centered=true] { + alignment: center; +} + QTabBar::tab { - border: 1px solid black; + border: 1px solid lightgrey; + border-radius: 3px; } QTabBar::tab:selected { @@ -53,13 +58,17 @@ } QTabBar::tab:!selected:hover { - background: #999; + background: #B5B5B5; } QTabBar::tab:top:!selected { margin-top: 3px; } +QTabBar::tab:top:!selected:hover { + margin-top: 1px; +} + QTabBar::tab:bottom:!selected { margin-bottom: 3px; } diff --git a/src/main/python/plotlyst/view/widget/input.py b/src/main/python/plotlyst/view/widget/input.py index 8f989b25a..320301612 100644 --- a/src/main/python/plotlyst/view/widget/input.py +++ b/src/main/python/plotlyst/view/widget/input.py @@ -56,6 +56,7 @@ def __init__(self, parent=None, height: int = 25): super(AutoAdjustableTextEdit, self).__init__(parent) self.textChanged.connect(self._resizeToContent) self._minHeight = height + self._resizedOnShow: bool = False self.setAcceptRichText(False) self.setFixedHeight(self._minHeight) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) @@ -66,13 +67,40 @@ def setText(self, text: str) -> None: @overrides def showEvent(self, a0: QtGui.QShowEvent) -> None: - self._resizeToContent() + if not self._resizedOnShow: + self._resizeToContent() + self._resizedOnShow = True def _resizeToContent(self): size = self.document().size() self.setFixedHeight(max(self._minHeight, size.height())) +class AutoAdjustableLineEdit(QLineEdit): + def __init__(self, parent=None, defaultWidth: int = 200): + super(AutoAdjustableLineEdit, self).__init__(parent) + self._padding = 10 + self._defaultWidth = defaultWidth + self._padding + self._resizedOnShow: bool = False + self.setFixedWidth(self._defaultWidth) + self.textChanged.connect(self._resizeToContent) + + @overrides + def showEvent(self, a0: QtGui.QShowEvent) -> None: + if not self._resizedOnShow: + self._resizeToContent() + self._resizedOnShow = True + + def _resizeToContent(self): + text = self.text().strip() + if text: + text_width = self.fontMetrics().boundingRect(text).width() + width = max(text_width + self._padding, self._defaultWidth) + self.setFixedWidth(width) + else: + self.setFixedWidth(self._defaultWidth) + + class TextBlockData(QTextBlockUserData): def __init__(self): super(TextBlockData, self).__init__() diff --git a/src/main/python/plotlyst/view/widget/world/__init__.py b/src/main/python/plotlyst/view/widget/world/__init__.py new file mode 100644 index 000000000..82d1fae9f --- /dev/null +++ b/src/main/python/plotlyst/view/widget/world/__init__.py @@ -0,0 +1 @@ +from .editor import WorldBuildingTreeView diff --git a/src/main/python/plotlyst/view/widget/world/editor.py b/src/main/python/plotlyst/view/widget/world/editor.py new file mode 100644 index 000000000..22cb32b81 --- /dev/null +++ b/src/main/python/plotlyst/view/widget/world/editor.py @@ -0,0 +1,182 @@ +""" +Plotlyst +Copyright (C) 2021-2023 Zsolt Kovari + +This file is part of Plotlyst. + +Plotlyst is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Plotlyst 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from functools import partial +from typing import Optional, Dict, Set + +from PyQt6.QtCore import pyqtSignal +from qthandy import vspacer, clear_layout +from qtmenu import MenuWidget, ActionTooltipDisplayMode + +from src.main.python.plotlyst.common import recursive +from src.main.python.plotlyst.core.domain import Novel, WorldBuildingEntity, WorldBuildingEntityType +from src.main.python.plotlyst.view.common import action +from src.main.python.plotlyst.view.icons import IconRegistry +from src.main.python.plotlyst.view.widget.tree import TreeView, ContainerNode, TreeSettings + + +class EntityAdditionMenu(MenuWidget): + entityTriggered = pyqtSignal(WorldBuildingEntity) + + def __init__(self, parent=None): + super(EntityAdditionMenu, self).__init__(parent) + self.setTooltipDisplayMode(ActionTooltipDisplayMode.DISPLAY_UNDER) + + self.addAction(action('Location', IconRegistry.location_icon(), + slot=lambda: self._triggered(WorldBuildingEntityType.SETTING), + tooltip='Physical location in the world')) + self.addAction(action('Entity', IconRegistry.world_building_icon(), + slot=lambda: self._triggered(WorldBuildingEntityType.ABSTRACT), + tooltip='Abstract entity in the world, e.g., magic')) + self.addAction(action('Social group', IconRegistry.group_icon(), + slot=lambda: self._triggered(WorldBuildingEntityType.GROUP), + tooltip='Social group in the world, e.g., a guild or an organization')) + self.addSeparator() + self.addAction(action('Item', IconRegistry.from_name('mdi.ring', '#b6a6ca'), + slot=lambda: self._triggered(WorldBuildingEntityType.ITEM), + tooltip='Relevant item in the world, e.g., an artifact')) + self.addSeparator() + self.addAction(action('Container', + slot=lambda: self._triggered(WorldBuildingEntityType.CONTAINER), + tooltip='General container to group worldbuilding entities together')) + + def _triggered(self, wdType: WorldBuildingEntityType): + if wdType == WorldBuildingEntityType.SETTING: + name = 'New location' + icon_name = 'fa5s.map-pin' + elif wdType == WorldBuildingEntityType.GROUP: + name = 'New group' + icon_name = 'mdi.account-group' + elif wdType == WorldBuildingEntityType.ITEM: + name = 'New item' + elif wdType == WorldBuildingEntityType.CONTAINER: + name = 'Container' + else: + name = 'New entity' + icon_name = '' + + entity = WorldBuildingEntity(name, icon=icon_name, type=wdType) + + self.entityTriggered.emit(entity) + + +class EntityNode(ContainerNode): + addEntity = pyqtSignal(WorldBuildingEntity) + + def __init__(self, entity: WorldBuildingEntity, parent=None, settings: Optional[TreeSettings] = None): + super(EntityNode, self).__init__(entity.name, parent=parent, settings=settings) + self._entity = entity + self.setPlusButtonEnabled(True) + self._additionMenu = EntityAdditionMenu(self._btnAdd) + self._additionMenu.entityTriggered.connect(self.addEntity.emit) + self.setPlusMenu(self._additionMenu) + self.refresh() + + def entity(self) -> WorldBuildingEntity: + return self._entity + + def refresh(self): + self._lblTitle.setText(self._entity.name) + + if self._entity.icon: + self._icon.setIcon(IconRegistry.from_name(self._entity.icon, self._entity.icon_color)) + self._icon.setVisible(True) + else: + self._icon.setHidden(True) + + +class RootNode(EntityNode): + + def __init__(self, entity: WorldBuildingEntity, parent=None, settings: Optional[TreeSettings] = None): + super(RootNode, self).__init__(entity, parent=parent, settings=settings) + self.setMenuEnabled(False) + self.setPlusButtonEnabled(False) + + +class WorldBuildingTreeView(TreeView): + entitySelected = pyqtSignal(WorldBuildingEntity) + + def __init__(self, parent=None, settings: Optional[TreeSettings] = None): + super(WorldBuildingTreeView, self).__init__(parent) + self._novel: Optional[Novel] = None + self._settings: Optional[TreeSettings] = settings + self._root: Optional[RootNode] = None + self._entities: Dict[WorldBuildingEntity, EntityNode] = {} + self._selectedEntities: Set[WorldBuildingEntity] = set() + self._centralWidget.setProperty('bg', True) + + def selectRoot(self): + self._root.select() + self._entitySelectionChanged(self._root, self._root.isSelected()) + + def setSettings(self, settings: TreeSettings): + self._settings = settings + + def setNovel(self, novel: Novel): + self._novel = novel + self._root = RootNode(self._novel.world.root_entity, settings=self._settings) + self._root.selectionChanged.connect(partial(self._entitySelectionChanged, self._root)) + self.refresh() + + def addEntity(self, entity: WorldBuildingEntity): + wdg = self.__initEntityWidget(entity) + self._root.addChild(wdg) + + def refresh(self): + def addChildWdg(parent: WorldBuildingEntity, child: WorldBuildingEntity): + childWdg = self.__initEntityWidget(child) + self._entities[parent].addChild(childWdg) + + self.clearSelection() + self._entities.clear() + clear_layout(self._centralWidget) + + self._entities[self._novel.world.root_entity] = self._root + self._centralWidget.layout().addWidget(self._root) + for entity in self._novel.world.root_entity.children: + wdg = self.__initEntityWidget(entity) + self._root.addChild(wdg) + recursive(entity, lambda parent: parent.children, addChildWdg) + self._centralWidget.layout().addWidget(vspacer()) + + def clearSelection(self): + for entity in self._selectedEntities: + self._entities[entity].deselect() + self._selectedEntities.clear() + + def _entitySelectionChanged(self, node: EntityNode, selected: bool): + if selected: + self.clearSelection() + self._selectedEntities.add(node.entity()) + self.entitySelected.emit(node.entity()) + elif node.entity() in self._selectedEntities: + self._selectedEntities.remove(node.entity()) + + def _addEntity(self, parent: EntityNode, entity: WorldBuildingEntity): + wdg = self.__initEntityWidget(entity) + parent.addChild(wdg) + parent.entity().children.append(entity) + + def __initEntityWidget(self, entity: WorldBuildingEntity) -> EntityNode: + node = EntityNode(entity, settings=self._settings) + node.selectionChanged.connect(partial(self._entitySelectionChanged, node)) + node.addEntity.connect(partial(self._addEntity, node)) + + self._entities[entity] = node + return node diff --git a/src/main/python/plotlyst/view/world_building_view.py b/src/main/python/plotlyst/view/world_building_view.py index 583dc7cc0..7f4309596 100644 --- a/src/main/python/plotlyst/view/world_building_view.py +++ b/src/main/python/plotlyst/view/world_building_view.py @@ -17,17 +17,20 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from PyQt6.QtCore import Qt -from PyQt6.QtGui import QPalette from overrides import overrides +from qthandy import incr_font, transparent from src.main.python.plotlyst.core.domain import Novel, WorldBuildingEntity from src.main.python.plotlyst.core.template import default_location_profiles from src.main.python.plotlyst.view._view import AbstractNovelView +from src.main.python.plotlyst.view.common import link_buttons_to_pages, ButtonPressResizeEventFilter from src.main.python.plotlyst.view.generated.world_building_view_ui import Ui_WorldBuildingView from src.main.python.plotlyst.view.icons import IconRegistry -from src.main.python.plotlyst.view.widget.world_building import WorldBuildingEditor, WorldBuildingItem, \ - WorldBuildingProfileTemplateView +from src.main.python.plotlyst.view.widget.input import AutoAdjustableLineEdit +from src.main.python.plotlyst.view.widget.tree import TreeSettings +from src.main.python.plotlyst.view.widget.utility import IconSelectorButton +from src.main.python.plotlyst.view.widget.world.editor import EntityAdditionMenu +from src.main.python.plotlyst.view.widget.world_building import WorldBuildingProfileTemplateView class WorldBuildingView(AbstractNovelView): @@ -37,33 +40,37 @@ def __init__(self, novel: Novel): self.ui = Ui_WorldBuildingView() self.ui.setupUi(self.widget) - self.widget.setPalette(QPalette(Qt.GlobalColor.white)) - self.ui.btnEditorToggle.setIcon(IconRegistry.document_edition_icon()) + self.ui.btnNew.setIcon(IconRegistry.plus_icon(color='white')) + self.ui.btnNew.installEventFilter(ButtonPressResizeEventFilter(self.ui.btnNew)) + self._additionMenu = EntityAdditionMenu(self.ui.btnNew) + self._additionMenu.entityTriggered.connect(self.ui.treeWorld.addEntity) - self._editor = WorldBuildingEditor(self.novel.world.root_entity) - self.ui.wdgEditorParent.layout().addWidget(self._editor) - self.ui.wdgSidebar.setVisible(self.ui.btnEditorToggle.isChecked()) self._settingTemplate = WorldBuildingProfileTemplateView(self.novel, default_location_profiles()[0]) - self.ui.wdgSidebar.layout().addWidget(self._settingTemplate) - self.ui.wdgSidebar.setDisabled(True) - self.ui.splitter.setSizes([500, 150]) + self.ui.splitter.setSizes([150, 500]) + self._lineName = AutoAdjustableLineEdit() + self._lineName.setPlaceholderText('Name') + transparent(self._lineName) + incr_font(self._lineName, 15) + self._btnIcon = IconSelectorButton() + self.ui.wdgName.layout().addWidget(self._btnIcon) + self.ui.wdgName.layout().addWidget(self._lineName) - self._editor.scene().modelChanged.connect(lambda: self.repo.update_novel(self.novel)) - self._editor.scene().selectionChanged.connect(self._selectionChanged) + self.ui.treeWorld.setSettings(TreeSettings(font_incr=2)) + self.ui.treeWorld.setNovel(self.novel) + self.ui.treeWorld.entitySelected.connect(self._selection_changed) + self.ui.treeWorld.selectRoot() - self.ui.btnEditorToggle.setChecked(False) + link_buttons_to_pages(self.ui.stackedWidget, [(self.ui.btnWorldView, self.ui.pageEditor), + (self.ui.btnHistoryView, self.ui.pageHistory)]) + self.ui.btnWorldView.setChecked(True) @overrides def refresh(self): pass - def _selectionChanged(self): - self._settingTemplate.clearValues() - - items = self._editor.scene().selectedItems() - if len(items) == 1 and isinstance(items[0], WorldBuildingItem): - self.ui.wdgSidebar.setEnabled(True) - entity: WorldBuildingEntity = items[0].entity() - self._settingTemplate.setLocation(entity) + def _selection_changed(self, entity: WorldBuildingEntity): + self._lineName.setText(entity.name) + if entity.icon: + self._btnIcon.selectIcon(entity.icon, entity.icon_color) else: - self.ui.wdgSidebar.setDisabled(True) + self._btnIcon.reset() diff --git a/ui/main_window.ui b/ui/main_window.ui index 4948c131d..5a93cc062 100644 --- a/ui/main_window.ui +++ b/ui/main_window.ui @@ -230,7 +230,7 @@ - false + true @@ -248,7 +248,7 @@ PointingHandCursor - World building (Not implemented yet) + World building @@ -268,6 +268,9 @@ true + + buttonGroup + diff --git a/ui/scenes_view.ui b/ui/scenes_view.ui index 90d06c353..83aea30f2 100644 --- a/ui/scenes_view.ui +++ b/ui/scenes_view.ui @@ -1158,8 +1158,8 @@ 0 0 - 98 - 28 + 84 + 16 @@ -1313,7 +1313,7 @@ - + diff --git a/ui/world_building_view.ui b/ui/world_building_view.ui index 7784dd1e8..343bf6c4f 100644 --- a/ui/world_building_view.ui +++ b/ui/world_building_view.ui @@ -6,19 +6,22 @@ 0 0 - 400 - 300 + 1082 + 655 Form + + true + 3 - 2 + 0 2 @@ -29,92 +32,412 @@ 2 - - - - PointingHandCursor - - - - - - true - - - true - - - true + + + + + 0 + 0 + - + + + + 0 + + + 20 + + + 0 + + + 2 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + PointingHandCursor + + + + + + 1 + + + true + + + false + + + true + + + buttonGroup + + + + + + + PointingHandCursor + + + + + + 3 + + + true + + + false + + + true + + + buttonGroup + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + Qt::Horizontal - - false + + + + + + 15 - - - - 0 + + 5 + + + + + + 130 + 16777215 + - - 0 + + PointingHandCursor - - 0 + + Add new scene - - 0 + + - - 0 + + Ctrl+N - - - + + true + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + PointingHandCursor + + + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + Qt::Horizontal + + + false + + 3 - 0 + 2 - 0 + 2 - 0 + 2 - 0 + 2 + + + + + 0 + 0 + + + + true + + + + + + 0 + + + + true + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 8 + + + 5 + + + 8 + + + 40 + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 500 + 16777215 + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + + + + + + + 0 + + + true + + + true + + + + true + + + Attributes + + + + + true + + + Topics + + + + + true + + + History + + + + + true + + + Notes + + + + + + + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + TextLabel + + + + + + + + + WorldBuildingTreeView + QTreeView +
src.main.python.plotlyst.view.widget.world
+
+
- - - btnEditorToggle - toggled(bool) - wdgSidebar - setVisible(bool) - - - 382 - 10 - - - 355 - 37 - - - - + + + +