From e5d2f01f278daa08035693d7f322c357a646e371 Mon Sep 17 00:00:00 2001
From: Zsolt Kovari <7029304+zkovari@users.noreply.github.com>
Date: Wed, 25 Sep 2024 12:21:37 +0200
Subject: [PATCH] Add Plotlyst roadmap panel (#1337)
* Add Plotlyst roadmap panel
* progress
* fix tests
* version and link
* tags
* filter by version
* task summary tooltip
* header
* counter
* version counters
* filter by tags
* space
* buttons
---
src/main/python/plotlyst/core/domain.py | 6 +-
src/main/python/plotlyst/view/home_view.py | 7 +-
src/main/python/plotlyst/view/main_window.py | 2 +
src/main/python/plotlyst/view/roadmap_view.py | 338 +++++++++++++
src/main/python/plotlyst/view/widget/task.py | 72 ++-
ui/home_view.ui | 23 +-
ui/roadmap_view.ui | 475 ++++++++++++++++++
7 files changed, 886 insertions(+), 37 deletions(-)
create mode 100644 src/main/python/plotlyst/view/roadmap_view.py
create mode 100644 ui/roadmap_view.ui
diff --git a/src/main/python/plotlyst/core/domain.py b/src/main/python/plotlyst/core/domain.py
index 2eb0ec347..3b01775b9 100644
--- a/src/main/python/plotlyst/core/domain.py
+++ b/src/main/python/plotlyst/core/domain.py
@@ -2231,7 +2231,7 @@ class SensoryPerception:
class SensoryDetail:
night_mode: bool = field(default=False, metadata=config(exclude=exclude_if_false))
perceptions: Dict[str, SensoryPerception] = field(default_factory=dict,
- metadata=config(exclude=exclude_if_empty))
+ metadata=config(exclude=exclude_if_empty))
@dataclass
@@ -2319,6 +2319,9 @@ class Task(CharacterBased):
summary: str = field(default='', metadata=config(exclude=exclude_if_empty))
character_id: Optional[uuid.UUID] = None
tags: List[str] = field(default_factory=list, metadata=config(exclude=exclude_if_empty))
+ web_link: str = field(default='', metadata=config(exclude=exclude_if_empty))
+ version: str = field(default='', metadata=config(exclude=exclude_if_empty))
+ beta: bool = field(default=False, metadata=config(exclude=exclude_if_false))
def __post_init__(self):
if self.creation_date is None:
@@ -2355,6 +2358,7 @@ def default_task_statues() -> List[TaskStatus]:
class Board:
tasks: List[Task] = field(default_factory=list)
statuses: List[TaskStatus] = field(default_factory=default_task_statues)
+ tags: Dict[str, SelectionItem] = field(default_factory=dict, metadata=config(exclude=exclude_if_empty))
class TemplateStoryStructureType(Enum):
diff --git a/src/main/python/plotlyst/view/home_view.py b/src/main/python/plotlyst/view/home_view.py
index 9cfcb04f5..328d77dde 100644
--- a/src/main/python/plotlyst/view/home_view.py
+++ b/src/main/python/plotlyst/view/home_view.py
@@ -43,6 +43,7 @@
from plotlyst.view.dialog.home import StoryCreationDialog
from plotlyst.view.generated.home_view_ui import Ui_HomeView
from plotlyst.view.icons import IconRegistry
+from plotlyst.view.roadmap_view import RoadmapView
from plotlyst.view.style.base import apply_border_image
from plotlyst.view.style.button import apply_button_palette_color
from plotlyst.view.widget.confirm import confirmed
@@ -112,7 +113,6 @@ def __init__(self):
self.ui.btnTutorials.setHidden(True)
self.ui.btnProgress.setHidden(True)
- self.ui.btnRoadmap.setHidden(True)
self.ui.lblWelcomeMain.setText(home_page_welcome_text)
@@ -181,6 +181,9 @@ def __init__(self):
(self.ui.btnProgress, self.ui.pageProgress),
(self.ui.btnRoadmap, self.ui.pageRoadmap)])
+ self._roadmapView = RoadmapView()
+ self.ui.pageRoadmap.layout().addWidget(self._roadmapView)
+
# self._tutorialsTreeView = TutorialsTreeView(settings=TreeSettings(font_incr=2))
# self._tutorialsTreeView.tutorialSelected.connect(self._tutorial_selected)
# self.ui.splitterTutorials.setSizes([150, 500])
@@ -201,7 +204,7 @@ def __init__(self):
# incr_font(self.ui.lineTutorialTitle, 10)
# bold(self.ui.lineTutorialTitle)
- self.ui.btnLibrary.setChecked(True)
+ self.ui.btnRoadmap.setChecked(True)
self.ui.stackWdgNovels.setCurrentWidget(self.ui.pageEmpty)
self._novels = client.novels()
diff --git a/src/main/python/plotlyst/view/main_window.py b/src/main/python/plotlyst/view/main_window.py
index a359f5acd..513bd56c4 100644
--- a/src/main/python/plotlyst/view/main_window.py
+++ b/src/main/python/plotlyst/view/main_window.py
@@ -189,6 +189,8 @@ def __init__(self, *args, **kwargs):
QApplication.instance().installEventFilter(CapitalizationEventFilter(self))
+ self.home_mode.setChecked(True)
+
@overrides
def closeEvent(self, event: QCloseEvent) -> None:
if language_tool_proxy.is_set():
diff --git a/src/main/python/plotlyst/view/roadmap_view.py b/src/main/python/plotlyst/view/roadmap_view.py
new file mode 100644
index 000000000..554624491
--- /dev/null
+++ b/src/main/python/plotlyst/view/roadmap_view.py
@@ -0,0 +1,338 @@
+"""
+Plotlyst
+Copyright (C) 2021-2024 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 .
+"""
+import datetime
+from functools import partial
+from typing import Optional, Dict, Set
+
+from PyQt6.QtCore import QEvent, QThreadPool, QSize, Qt, pyqtSignal
+from PyQt6.QtGui import QFont
+from PyQt6.QtWidgets import QWidget, QSizePolicy, QFrame
+from overrides import overrides
+from qthandy import clear_layout, hbox, spacer, margins, vbox, incr_font, retain_when_hidden, decr_icon, translucent, \
+ decr_font, transparent, vspacer, italic
+from qthandy.filter import VisibilityToggleEventFilter, OpacityEventFilter
+
+from plotlyst.common import PLOTLYST_SECONDARY_COLOR, PLOTLYST_MAIN_COLOR, RELAXED_WHITE_COLOR
+from plotlyst.core.domain import Board, Task, TaskStatus
+from plotlyst.core.template import SelectionItem
+from plotlyst.env import app_env
+from plotlyst.service.resource import JsonDownloadWorker, JsonDownloadResult
+from plotlyst.view.common import push_btn, spin, shadow, tool_btn, open_url, ButtonPressResizeEventFilter
+from plotlyst.view.generated.roadmap_view_ui import Ui_RoadmapView
+from plotlyst.view.icons import IconRegistry
+from plotlyst.view.layout import group
+from plotlyst.view.style.button import apply_button_palette_color
+from plotlyst.view.widget.input import AutoAdjustableTextEdit
+from plotlyst.view.widget.task import BaseStatusColumnWidget
+from plotlyst.view.widget.tree import TreeView, EyeToggleNode
+
+tags_counter: Dict[str, int] = {}
+versions_counter: Dict[str, int] = {}
+
+
+class TagsTreeView(TreeView):
+ toggled = pyqtSignal(str, bool)
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+
+ def setTags(self, tags: Dict[str, SelectionItem]):
+ self.clear()
+ for k, v in tags.items():
+ node = EyeToggleNode(f'{k.capitalize()} ({tags_counter.get(k, 0)})',
+ IconRegistry.from_name(v.icon, color=v.icon_color))
+ node.setToolTip(v.text)
+ node.toggled.connect(partial(self.toggled.emit, k))
+ self._centralWidget.layout().addWidget(node)
+
+ self._centralWidget.layout().addWidget(vspacer())
+
+ def clear(self):
+ clear_layout(self._centralWidget)
+
+
+class RoadmapTaskWidget(QFrame):
+
+ def __init__(self, task: Task, tags: Dict[str, SelectionItem], parent=None):
+ super().__init__(parent)
+ self._task: Task = task
+
+ self.setProperty('relaxed-white-bg', True)
+ self.setProperty('rounded', True)
+
+ vbox(self, margin=5)
+ self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
+ self.setMinimumHeight(75)
+ shadow(self, 3)
+
+ self._textTitle = AutoAdjustableTextEdit(self)
+ self._textTitle.setPlaceholderText('New task')
+ self._textTitle.setText(task.title)
+ self.setToolTip(task.summary)
+ transparent(self._textTitle)
+ self._textTitle.setReadOnly(True)
+ font = QFont(app_env.sans_serif_font())
+ font.setWeight(QFont.Weight.Medium)
+ self._textTitle.setFont(font)
+ incr_font(self._textTitle)
+
+ self._btnOpenInExternal = tool_btn(IconRegistry.from_name('fa5s.external-link-alt', 'grey'), transparent_=True,
+ tooltip='Open in browser')
+ self._btnOpenInExternal.clicked.connect(lambda: open_url(self._task.web_link))
+ retain_when_hidden(self._btnOpenInExternal)
+ decr_icon(self._btnOpenInExternal, 4)
+
+ top_wdg = group(self._textTitle, spacer(), self._btnOpenInExternal, margin=0, spacing=1)
+ self.layout().addWidget(top_wdg, alignment=Qt.AlignmentFlag.AlignTop)
+
+ self._wdgBottom = QWidget()
+ retain_when_hidden(self._wdgBottom)
+ hbox(self._wdgBottom)
+
+ for tag_name in self._task.tags:
+ tag = tags.get(tag_name)
+ if tag:
+ btn = tool_btn(IconRegistry.from_name(tag.icon, tag.icon_color), transparent_=True, icon_resize=False,
+ pointy_=False, tooltip=tag.text)
+ decr_icon(btn, 4)
+ translucent(btn, 0.7)
+ self._wdgBottom.layout().addWidget(btn)
+
+ self._wdgBottom.layout().addWidget(spacer())
+ if self._task.version == 'Plus':
+ self._btnVersion = push_btn(text=self._task.version, properties=['transparent'],
+ tooltip='Feature will be available in Plotlyst Plus', icon_resize=False,
+ pointy_=False)
+ self._btnVersion.setIcon(IconRegistry.from_name('mdi.certificate', color=PLOTLYST_MAIN_COLOR))
+ apply_button_palette_color(self._btnVersion, PLOTLYST_MAIN_COLOR)
+ decr_font(self._btnVersion)
+ decr_icon(self._btnVersion, 2)
+ self._wdgBottom.layout().addWidget(self._btnVersion)
+
+ self.layout().addWidget(self._wdgBottom, alignment=Qt.AlignmentFlag.AlignBottom)
+
+ self.installEventFilter(VisibilityToggleEventFilter(self._btnOpenInExternal, self))
+
+ def task(self) -> Task:
+ return self._task
+
+
+class RoadmapStatusColumn(BaseStatusColumnWidget):
+
+ def __init__(self, status: TaskStatus, parent=None):
+ super().__init__(status, parent, collapseEnabled=False, headerAdditionEnabled=False)
+
+ def addTask(self, task: Task, board: Board) -> RoadmapTaskWidget:
+ wdg = RoadmapTaskWidget(task, board.tags, self)
+ self._container.layout().insertWidget(self._container.layout().count() - 1, wdg,
+ alignment=Qt.AlignmentFlag.AlignTop)
+
+ self._header.updateTitle(self._container.layout().count() - 1)
+ return wdg
+
+
+class RoadmapBoardWidget(QWidget):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ hbox(self, spacing=20)
+ self._statusColumns: Dict[str, RoadmapStatusColumn] = {}
+ self._tasks: Dict[Task, RoadmapTaskWidget] = {}
+ self._tagFilters: Set[str] = set()
+
+ def setBoard(self, board: Board):
+ clear_layout(self)
+ self._statusColumns.clear()
+ self._tasks.clear()
+ self._tagFilters.clear()
+ self._version: str = ''
+ self._beta: bool = False
+
+ for status in board.statuses:
+ column = RoadmapStatusColumn(status)
+ self.layout().addWidget(column)
+ self._statusColumns[str(status.id)] = column
+
+ for task in board.tasks:
+ column = self._statusColumns.get(str(task.status_ref))
+ wdg = column.addTask(task, board)
+ self._tasks[task] = wdg
+ for tag in task.tags:
+ if tag not in tags_counter:
+ tags_counter[tag] = 0
+ tags_counter[tag] += 1
+ if task.beta:
+ versions_counter['Beta'] += 1
+ if task.version:
+ versions_counter[task.version] += 1
+
+ _spacer = spacer()
+ _spacer.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
+ self.layout().addWidget(_spacer)
+ margins(self, left=20)
+
+ def showAll(self):
+ self._version = ''
+ self._beta = False
+
+ for task, wdg in self._tasks.items():
+ wdg.setVisible(self._filter(task))
+
+ def filterVersion(self, version: str):
+ self._version = version
+ self._beta = False
+
+ for task, wdg in self._tasks.items():
+ wdg.setVisible(self._filter(task))
+
+ def filterBeta(self):
+ self._version = ''
+ self._beta = True
+
+ for task, wdg in self._tasks.items():
+ wdg.setVisible(self._filter(task))
+
+ def filterTag(self, tag: str, filtered: bool):
+ if filtered:
+ self._tagFilters.add(tag)
+ else:
+ self._tagFilters.remove(tag)
+
+ for task, wdg in self._tasks.items():
+ wdg.setVisible(self._filter(task))
+
+ def _filter(self, task: Task) -> bool:
+ if self._version and task.version != self._version:
+ return False
+ if self._beta and not task.beta:
+ return False
+
+ return self._filteredByTags(task)
+
+ def _filteredByTags(self, task: Task) -> bool:
+ if not self._tagFilters:
+ return True
+
+ for tag in task.tags:
+ if tag in self._tagFilters:
+ return True
+ return False
+
+
+class RoadmapView(QWidget, Ui_RoadmapView):
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setupUi(self)
+
+ self.btnRoadmapIcon.setIcon(IconRegistry.from_name('fa5s.road'))
+ self.btnPlus.setIcon(IconRegistry.from_name('mdi.certificate', color_on=PLOTLYST_SECONDARY_COLOR))
+ self.btnVisitRoadmap.setIcon(IconRegistry.from_name('fa5s.external-link-alt'))
+ self.btnVisitRoadmap.installEventFilter(ButtonPressResizeEventFilter(self.btnVisitRoadmap))
+ self.btnVisitRoadmap.clicked.connect(lambda: open_url('https://plotlyst.featurebase.app/roadmap'))
+ decr_icon(self.btnVisitRoadmap, 2)
+ decr_font(self.btnVisitRoadmap)
+ italic(self.btnVisitRoadmap)
+ self.btnVisitRoadmap.installEventFilter(OpacityEventFilter(self.btnVisitRoadmap, enterOpacity=0.7))
+
+ self.btnSubmitRequest.setIcon(IconRegistry.from_name('mdi.comment-text', RELAXED_WHITE_COLOR))
+ self.btnSubmitRequest.installEventFilter(ButtonPressResizeEventFilter(self.btnSubmitRequest))
+ self.btnSubmitRequest.clicked.connect(lambda: open_url('https://plotlyst.featurebase.app/'))
+
+ self.splitter.setSizes([150, 550])
+
+ self._last_fetched = None
+ self._downloading = False
+ self._board: Optional[Board] = None
+ self._thread_pool = QThreadPool()
+
+ self._roadmapWidget = RoadmapBoardWidget()
+ self.scrollAreaWidgetContents.layout().addWidget(self._roadmapWidget)
+
+ self._tagsTree = TagsTreeView()
+ self.wdgCategoriesParent.layout().addWidget(self._tagsTree)
+ self._tagsTree.toggled.connect(self._roadmapWidget.filterTag)
+
+ self.btnAll.clicked.connect(self._roadmapWidget.showAll)
+ self.btnFree.clicked.connect(lambda: self._roadmapWidget.filterVersion('Free'))
+ self.btnPlus.clicked.connect(lambda: self._roadmapWidget.filterVersion('Plus'))
+ self.btnBeta.clicked.connect(self._roadmapWidget.filterBeta)
+
+ self.wdgLoading.setHidden(True)
+
+ @overrides
+ def showEvent(self, event: QEvent):
+ super().showEvent(event)
+
+ if self._downloading:
+ return
+
+ if self._last_fetched is None or (datetime.datetime.now() - self._last_fetched).total_seconds() > 86400:
+ self._handle_downloading_status(True)
+ self._download_data()
+
+ def _download_data(self):
+ result = JsonDownloadResult()
+ runnable = JsonDownloadWorker("https://raw.githubusercontent.com/plotlyst/feed/refs/heads/main/posts.json",
+ result)
+ result.finished.connect(self._handle_downloaded_data)
+ result.failed.connect(self._handle_download_failure)
+ self._thread_pool.start(runnable)
+
+ def _handle_downloaded_data(self, data):
+ self.btnAll.setChecked(True)
+ tags_counter.clear()
+ versions_counter.clear()
+ versions_counter['Free'] = 0
+ versions_counter['Plus'] = 0
+ versions_counter['Beta'] = 0
+
+ self._board = Board.from_dict(data)
+ self._roadmapWidget.setBoard(self._board)
+ self._tagsTree.setTags(self._board.tags)
+
+ self.btnFree.setText(f'Free ({versions_counter.get("Free", 0)})')
+ self.btnPlus.setText(f'Plus ({versions_counter.get("Plus", 0)})')
+ self.btnBeta.setText(f'Beta ({versions_counter.get("Beta", 0)})')
+
+ now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
+ self.lblLastUpdated.setText(f"Last updated: {now}")
+ self._last_fetched = datetime.datetime.now()
+
+ self._handle_downloading_status(False)
+
+ def _handle_download_failure(self, status_code: int, message: str):
+ if self._board is None:
+ self.lblLastUpdated.setText("Failed to update data.")
+ self._handle_downloading_status(False)
+
+ def _handle_downloading_status(self, loading: bool):
+ self._downloading = loading
+ self.scrollAreaWidgetContents.setDisabled(loading)
+ self.splitter.setHidden(loading)
+ self.wdgTopSelectors.setHidden(loading)
+ self.wdgLoading.setVisible(loading)
+ if loading:
+ btn = push_btn(transparent_=True)
+ btn.setIconSize(QSize(128, 128))
+ self.wdgLoading.layout().addWidget(btn,
+ alignment=Qt.AlignmentFlag.AlignCenter | Qt.AlignmentFlag.AlignVCenter)
+ spin(btn, PLOTLYST_SECONDARY_COLOR)
+ else:
+ clear_layout(self.wdgLoading)
diff --git a/src/main/python/plotlyst/view/widget/task.py b/src/main/python/plotlyst/view/widget/task.py
index 71de25697..7d5c88dad 100644
--- a/src/main/python/plotlyst/view/widget/task.py
+++ b/src/main/python/plotlyst/view/widget/task.py
@@ -40,7 +40,7 @@
TaskChangedFromWip
from plotlyst.service.persistence import RepositoryPersistenceManager
from plotlyst.view.common import ButtonPressResizeEventFilter, shadow, action, tool_btn, \
- any_menu_visible
+ any_menu_visible, insert_before_the_end
from plotlyst.view.icons import IconRegistry
from plotlyst.view.layout import group
from plotlyst.view.widget.button import CollapseButton, TaskTagSelector
@@ -186,7 +186,8 @@ class _StatusHeader(QFrame):
collapseToggled = pyqtSignal(bool)
addTask = pyqtSignal()
- def __init__(self, status: TaskStatus, parent=None):
+ def __init__(self, status: TaskStatus, parent=None, collapseEnabled: bool = True,
+ headerAdditionEnabled: bool = True):
super(_StatusHeader, self).__init__(parent)
self._status = status
self.setStyleSheet(f'''_StatusHeader {{
@@ -199,7 +200,10 @@ def __init__(self, status: TaskStatus, parent=None):
bold(self._title)
self.updateTitle()
self._btnCollapse = CollapseButton(Qt.Edge.BottomEdge, Qt.Edge.LeftEdge, self)
- self.installEventFilter(VisibilityToggleEventFilter(self._btnCollapse, self))
+ if collapseEnabled:
+ self.installEventFilter(VisibilityToggleEventFilter(self._btnCollapse, self))
+ else:
+ self._btnCollapse.setHidden(True)
shadow(self)
self._btnAdd = QToolButton()
@@ -219,7 +223,10 @@ def __init__(self, status: TaskStatus, parent=None):
''')
pointy(self._btnAdd)
self._btnAdd.installEventFilter(ButtonPressResizeEventFilter(self._btnAdd))
- self.installEventFilter(VisibilityToggleEventFilter(self._btnAdd, self))
+ if headerAdditionEnabled:
+ self.installEventFilter(VisibilityToggleEventFilter(self._btnAdd, self))
+ else:
+ self._btnAdd.setHidden(True)
self.layout().addWidget(self._title)
self.layout().addWidget(self._btnCollapse)
@@ -237,25 +244,42 @@ def updateTitle(self, childrenCount: int = 0):
self._title.setText(f'{self._status.text.upper()} {suffix}')
-class StatusColumnWidget(QFrame):
- taskChanged = pyqtSignal(Task)
- taskDeleted = pyqtSignal(Task)
- taskResolved = pyqtSignal(Task)
-
- def __init__(self, novel: Novel, status: TaskStatus, parent=None):
- super(StatusColumnWidget, self).__init__(parent)
- self._novel = novel
+class BaseStatusColumnWidget(QFrame):
+ def __init__(self, status: TaskStatus, parent=None, collapseEnabled: bool = True,
+ headerAdditionEnabled: bool = True):
+ super().__init__(parent)
self._status = status
+
vbox(self, 1, 20)
- self._header = _StatusHeader(self._status)
+ self._header = _StatusHeader(self._status, collapseEnabled=collapseEnabled,
+ headerAdditionEnabled=headerAdditionEnabled)
self._header.collapseToggled.connect(self._collapseToggled)
- self._container = QWidget(self)
+ self._container = QFrame(self)
+ self._container.setProperty('darker-bg', True)
+ self._container.setProperty('rounded', True)
spacing = 6 if app_env.is_mac() else 12
- vbox(self._container, margin=5, spacing=spacing)
+ vbox(self._container, margin=10, spacing=spacing)
+ self._container.layout().addWidget(vspacer())
+
self.setMaximumWidth(TASK_WIDGET_MAX_WIDTH)
self.layout().addWidget(self._header)
self.layout().addWidget(self._container)
- self.layout().addWidget(vspacer())
+
+ def _collapseToggled(self, toggled: bool):
+ for i in range(self._container.layout().count()):
+ item = self._container.layout().itemAt(i)
+ if item.widget() and isinstance(item.widget(), TaskWidget):
+ item.widget().setHidden(toggled)
+
+
+class StatusColumnWidget(BaseStatusColumnWidget):
+ taskChanged = pyqtSignal(Task)
+ taskDeleted = pyqtSignal(Task)
+ taskResolved = pyqtSignal(Task)
+
+ def __init__(self, novel: Novel, status: TaskStatus, parent=None):
+ super(StatusColumnWidget, self).__init__(status, parent)
+ self._novel = novel
self._btnAdd = QPushButton('New Task', self)
self._btnAdd.setIcon(IconRegistry.plus_icon('grey'))
@@ -265,7 +289,7 @@ def __init__(self, novel: Novel, status: TaskStatus, parent=None):
self._btnAdd.installEventFilter(ButtonPressResizeEventFilter(self._btnAdd))
self._btnAdd.installEventFilter(OpacityEventFilter(self._btnAdd))
- self._container.layout().addWidget(self._btnAdd, alignment=Qt.AlignmentFlag.AlignLeft)
+ insert_before_the_end(self._container, self._btnAdd, alignment=Qt.AlignmentFlag.AlignLeft)
self.installEventFilter(VisibilityToggleEventFilter(self._btnAdd, self))
self.setAcceptDrops(True)
@@ -281,7 +305,7 @@ def __init__(self, novel: Novel, status: TaskStatus, parent=None):
def event_received(self, event: Event):
if isinstance(event, CharacterDeletedEvent):
- for i in range(self._container.layout().count() - 1):
+ for i in range(self._container.layout().count() - 2):
item = self._container.layout().itemAt(i)
if item.widget():
taskWdg: TaskWidget = item.widget()
@@ -294,7 +318,7 @@ def status(self) -> TaskStatus:
def addTask(self, task: Task, edit: bool = False) -> TaskWidget:
wdg = TaskWidget(task, self)
- self._container.layout().insertWidget(self._container.layout().count() - 1, wdg,
+ self._container.layout().insertWidget(self._container.layout().count() - 2, wdg,
alignment=Qt.AlignmentFlag.AlignTop)
wdg.installEventFilter(
DragEventFilter(self, mimeType=TASK_MIME_TYPE, dataFunc=self._grabbedTaskData,
@@ -310,7 +334,7 @@ def addTask(self, task: Task, edit: bool = False) -> TaskWidget:
if self._status.wip:
emit_event(self._novel, TaskChangedToWip(self, task))
- self._header.updateTitle(self._container.layout().count() - 1)
+ self._header.updateTitle(self._container.layout().count() - 2)
return wdg
def _addNewTask(self):
@@ -324,12 +348,6 @@ def _deleteTask(self, taskWidget: TaskWidget):
self.__removeTaskWidget(taskWidget)
self.taskDeleted.emit(task)
- def _collapseToggled(self, toggled: bool):
- for i in range(self._container.layout().count()):
- item = self._container.layout().itemAt(i)
- if item.widget() and isinstance(item.widget(), TaskWidget):
- item.widget().setHidden(toggled)
-
def _grabbedTaskData(self, widget: TaskWidget):
return widget.task()
@@ -365,7 +383,7 @@ def __removeTaskWidget(self, taskWidget):
taskWidget.setHidden(True)
self._container.layout().removeWidget(taskWidget)
gc(taskWidget)
- self._header.updateTitle(self._container.layout().count() - 1)
+ self._header.updateTitle(self._container.layout().count() - 2)
class BoardWidget(QWidget):
diff --git a/ui/home_view.ui b/ui/home_view.ui
index df640e987..b5d568537 100644
--- a/ui/home_view.ui
+++ b/ui/home_view.ui
@@ -254,6 +254,22 @@
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 10
+
+
+
+
-
@@ -1284,13 +1300,6 @@ It offers a range of tools to support various stages of novel writing, including
0
-
-
-
-
- Coming soon
-
-
-
diff --git a/ui/roadmap_view.ui b/ui/roadmap_view.ui
new file mode 100644
index 000000000..4a420cf0b
--- /dev/null
+++ b/ui/roadmap_view.ui
@@ -0,0 +1,475 @@
+
+
+ RoadmapView
+
+
+
+ 0
+ 0
+ 554
+ 453
+
+
+
+ Form
+
+
+
+ 3
+
+
+ 15
+
+
+ 5
+
+
+ 15
+
+
+ 5
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Plotlyst roadmap
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+
+
+ true
+
+
+
+ -
+
+
+ PointingHandCursor
+
+
+ Visit online board
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+
+ 0
+
+
+ 15
+
+
+ 0
+
+
+ 5
+
+
+ 0
+
+
-
+
+
+ Planned, in-progress, and completed tasks for both the Free and Plotlyst Plus versions. Beta: upcoming features that are currently available in the Beta version.
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+
+ 3
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 70
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Display all tasks
+
+
+ All
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+ buttonGroup
+
+
+
+ -
+
+
+
+ 70
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Display tasks that belong to the free verison of Plotlyst
+
+
+ Free
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ true
+
+
+ buttonGroup
+
+
+
+ -
+
+
+
+ 70
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Display tasks that belong to Plotlyst Plus
+
+
+ Plus
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ true
+
+
+ buttonGroup
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+
+ 70
+ 0
+
+
+
+ PointingHandCursor
+
+
+ Display done tasks that are currently available in the Beta version
+
+
+ Beta
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ true
+
+
+ buttonGroup
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ PointingHandCursor
+
+
+ Submit a request
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::Horizontal
+
+
+ 10
+
+
+ false
+
+
+ true
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 3
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+
+
+
+ QFrame::NoFrame
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 343
+ 176
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+
+
+ 20
+
+
+ 15
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+