Skip to content

Commit a15cea9

Browse files
committed
show scene images in read book widget
1 parent 3c82ef6 commit a15cea9

8 files changed

Lines changed: 356 additions & 28 deletions

File tree

src/calibre/gui2/cyoa/data.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def prefs() -> JSONConfig:
6363
ans.defaults['game_splitter_state'] = None
6464
ans.defaults['turn_timeout_minutes'] = 5
6565
ans.defaults['text_display'] = {}
66+
ans.defaults['read_story_show_images'] = True
6667
return ans
6768

6869

@@ -76,6 +77,16 @@ def game_splitter_state() -> bytes:
7677
return bytes(prefs()['game_splitter_state'] or b'')
7778

7879

80+
def read_story_show_images() -> bool:
81+
# Whether the dialog that shows the story so far as a book displays the
82+
# pictures of the scenes of its turns as illustrations.
83+
return bool(prefs()['read_story_show_images'])
84+
85+
86+
def set_read_story_show_images(val: bool) -> None:
87+
prefs().set('read_story_show_images', bool(val))
88+
89+
7990
def turn_timeout_minutes() -> int:
8091
return int(prefs()['turn_timeout_minutes'])
8192

src/calibre/gui2/cyoa/game.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ def toolbar_action(icon: str, text: str, tooltip: str, receiver: Callable[[], No
282282
ab.setToolTip('<p>' + _('Submit your action to the AI. You can also press {} in the box above').format('Ctrl+Enter'))
283283
ab.clicked.connect(self.take_action)
284284
h.addWidget(ab)
285-
self.interesting_button = ib = QPushButton(QIcon.ic('ai.png'), _('Something &interesting happens'), input_panel)
285+
self.interesting_button = ib = QPushButton(QIcon.ic('random.png'), _('Something &interesting happens'), input_panel)
286286
ib.setToolTip('<p>' + _('Instead of taking an action yourself, have the AI make something unexpected and interesting happen next'))
287287
ib.clicked.connect(self.interesting_event)
288288
h.addWidget(ib), h.addStretch()
@@ -1342,7 +1342,7 @@ def read_story(self) -> None:
13421342
# The story so far as a book: all of its chapters, not just the one
13431343
# being played, in a dialog of their own.
13441344
if self.state is not None and self.state.turns:
1345-
ReadStoryDialog(self.state, self).exec()
1345+
ReadStoryDialog(self.state, self.images, self).exec()
13461346

13471347
def change_settings(self) -> None:
13481348
if SettingsDialog(self).exec() != Dialog.DialogCode.Accepted:

src/calibre/gui2/cyoa/main.py

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,38 @@
44
# The main window for the "Create Your Own Adventure" game. It shows the
55
# welcome screen until the AI used to generate the story is configured, then
66
# either the world creation flow or, when a game is already in progress, the
7-
# game itself.
7+
# game itself. Like the E-book viewer and the editor, the game is a program
8+
# of its own: it is normally started as a separate process, either from the
9+
# command line or by the calibre GUI via
10+
# job_manager.launch_gui_app('cyoa'), and keeps running after the calibre
11+
# GUI that launched it is closed.
812
# Run with: calibre-debug -c 'from calibre.gui2.cyoa.main import main; main()'
913

10-
from qt.core import QFont, QIcon, QSize, QStackedWidget
14+
import os
15+
import sys
16+
from collections.abc import Sequence
17+
from contextlib import closing
18+
19+
from qt.core import QFont, QIcon, QSize, QStackedWidget, Qt
1120

1221
from calibre.ai.cyoa import PROTAGONIST_ID, GeneratedWorld, StoryStyle, start_game
1322
from calibre.constants import CYOA_APP_UID, islinux
14-
from calibre.gui2 import Application, error_dialog, gprefs
23+
from calibre.gui2 import Application, error_dialog, gprefs, setup_gui_option_parser
1524
from calibre.gui2.cyoa import data
1625
from calibre.gui2.cyoa.game import GameWidget
1726
from calibre.gui2.cyoa.welcome import WelcomeWidget
1827
from calibre.gui2.cyoa.world import CreateWorldWidget
28+
from calibre.gui2.listener import Listener, send_message_in_process
1929
from calibre.gui2.main_window import MainWindow
30+
from calibre.ptempfile import reset_base_dir
31+
from calibre.utils.config import OptionParser
32+
from calibre.utils.ipc import cyoa_socket_address
2033
from calibre.utils.localization import _
34+
from calibre.utils.lock import SingleInstance
35+
36+
# Only one process can play at a time, as two of them would overwrite each
37+
# other's auto-saved game, see main().
38+
SINGLE_INSTANCE_NAME = 'calibre_cyoa'
2139

2240

2341
class CYOAMainWindow(MainWindow):
@@ -90,22 +108,90 @@ def abandon_game(self) -> None:
90108
data.set_current_game('')
91109
self.show_appropriate_page()
92110

111+
def message_from_other_instance(self, msg: bytes) -> None:
112+
# Another process was started to play the game. Since there can be
113+
# only one, it asks this one to come to the front instead, see
114+
# main().
115+
self.raise_and_focus()
116+
117+
118+
def option_parser() -> OptionParser:
119+
from calibre.gui2.main_window import option_parser as base_option_parser
120+
121+
parser = base_option_parser(
122+
_(
123+
'''\
124+
%prog [options]
125+
126+
Play a "Create Your Own Adventure" game, in which the story is written by an AI as you play it.
127+
'''
128+
)
129+
)
130+
setup_gui_option_parser(parser)
131+
return parser
132+
133+
134+
def run_gui(app: Application, listener: Listener | None = None) -> None:
135+
w = CYOAMainWindow()
136+
w.set_exception_handler()
137+
app.shutdown_signal_received.connect(w.close)
138+
if listener is not None:
139+
listener.message_received.connect(w.message_from_other_instance, type=Qt.ConnectionType.QueuedConnection)
140+
w.show()
141+
app.exec()
142+
del w
143+
93144

94-
def main() -> None:
95-
override = 'calibre-ebook-viewer' if islinux else None
96-
app = Application([], override_program_name=override, windows_app_uid=CYOA_APP_UID)
145+
def main(args: Sequence[str] = sys.argv) -> None:
146+
# Ensure the game can continue to be played if the calibre GUI that
147+
# launched it is closed, which would otherwise take its temporary
148+
# directory away.
149+
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
150+
reset_base_dir()
151+
args = list(args)
152+
override = 'calibre-cyoa' if islinux else None
153+
app = Application(args, override_program_name=override, windows_app_uid=CYOA_APP_UID)
154+
option_parser().parse_args(args)
97155
fi = gprefs['font']
98156
if fi is not None:
99157
font = QFont(*(fi[:4]))
100158
s = gprefs.get('font_stretch', None)
101159
if s is not None:
102160
font.setStretch(s)
103161
app.setFont(font)
104-
w = CYOAMainWindow()
105-
w.set_exception_handler()
106-
w.show()
107-
app.exec()
108-
del w
162+
app.setWindowIcon(QIcon.ic('ai.png'))
163+
# Two processes playing at the same time would overwrite each other's
164+
# auto-saved game, so a second launch asks the one already running to
165+
# come to the front and exits.
166+
with SingleInstance(SINGLE_INSTANCE_NAME) as si:
167+
if not si:
168+
try:
169+
send_message_in_process(b'raise-window', address=cyoa_socket_address())
170+
except Exception as err:
171+
error_dialog(
172+
None,
173+
_('Failed to connect'),
174+
_('Could not connect to the already running game window, try restarting it.'),
175+
det_msg=str(err),
176+
show=True,
177+
)
178+
raise SystemExit(1)
179+
else:
180+
try:
181+
listener = Listener(address=cyoa_socket_address(), parent=app)
182+
listener.start_listening()
183+
except Exception as err:
184+
error_dialog(
185+
None,
186+
_('Failed to start listener'),
187+
_('Could not start the listener used to ensure only one game is played at a time. Try rebooting your computer.'),
188+
det_msg=str(err),
189+
show=True,
190+
)
191+
run_gui(app)
192+
else:
193+
with closing(listener):
194+
run_gui(app, listener)
109195
del app
110196

111197

0 commit comments

Comments
 (0)