Skip to content

Commit

Permalink
Start new worldbuilding panel (#631)
Browse files Browse the repository at this point in the history
* Worldbuilding panel

* Worldbuilding panel

* New tab style

* Add AutoAdjustableLineEdit

* progress with panel

* tree

* fix test

* plus btn enable

* settings

* settings

* change base color

* splitter size

* bg colors

* Add new entities to tree

* fix
  • Loading branch information
zkovari authored Jul 24, 2023
1 parent b596815 commit 57bde5e
Show file tree
Hide file tree
Showing 14 changed files with 663 additions and 105 deletions.
7 changes: 4 additions & 3 deletions src/main/python/plotlyst/core/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,7 @@ class WorldBuildingEntityType(Enum):
SETTING = 2
GROUP = 3
ITEM = 4
CONTAINER = 5


@dataclass
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions src/main/python/plotlyst/test/view/test_scenes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/main/python/plotlyst/view/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
1 change: 0 additions & 1 deletion src/main/python/plotlyst/view/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/main/python/plotlyst/view/manuscript_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/main/python/plotlyst/view/style/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
style = '''
* {
icon-size: 20px;
color: #040406;
}
QToolTip {
Expand Down
27 changes: 18 additions & 9 deletions src/main/python/plotlyst/view/style/tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
30 changes: 29 additions & 1 deletion src/main/python/plotlyst/view/widget/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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__()
Expand Down
1 change: 1 addition & 0 deletions src/main/python/plotlyst/view/widget/world/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .editor import WorldBuildingTreeView
182 changes: 182 additions & 0 deletions src/main/python/plotlyst/view/widget/world/editor.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
"""
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
Loading

0 comments on commit 57bde5e

Please sign in to comment.