mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -04:00
E-book viewer: Add an option to allow only a single book to be viewed at a time. Trying to view a second book will cause it to replace the currently viewed book. Fixes #1526504 [view: reuse current instance option](https://bugs.launchpad.net/calibre/+bug/1526504)
This commit is contained in:
parent
8c8ae49884
commit
ca0854ce2e
@ -291,8 +291,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
def restore_defaults(self):
|
||||
opts = config('').parse()
|
||||
self.load_options(opts)
|
||||
from calibre.gui2.viewer.main import dprefs
|
||||
from calibre.gui2.viewer.main import dprefs, vprefs
|
||||
self.word_lookups = dprefs.defaults['word_lookups']
|
||||
self.opt_singleinstance.setChecked(vprefs.defaults['singleinstance'])
|
||||
|
||||
def load_options(self, opts):
|
||||
self.opt_remember_window_size.setChecked(opts.remember_window_size)
|
||||
@ -344,6 +345,8 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
setattr(self, 'current_%s_color'%x, getattr(opts, '%s_color'%x))
|
||||
self.update_sample_colors()
|
||||
self.opt_show_controls.setChecked(opts.show_controls)
|
||||
from calibre.gui2.viewer.main import vprefs
|
||||
self.opt_singleinstance.setChecked(bool(vprefs['singleinstance']))
|
||||
|
||||
def change_color(self, which, reset=False):
|
||||
if reset:
|
||||
@ -429,5 +432,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
c.set('show_controls', self.opt_show_controls.isChecked())
|
||||
for x in ('top', 'bottom', 'side'):
|
||||
c.set(x+'_margin', int(getattr(self, 'opt_%s_margin'%x).value()))
|
||||
from calibre.gui2.viewer.main import dprefs
|
||||
from calibre.gui2.viewer.main import dprefs, vprefs
|
||||
dprefs['word_lookups'] = self.word_lookups
|
||||
vprefs['singleinstance'] = self.opt_singleinstance.isChecked()
|
||||
|
@ -68,7 +68,7 @@ QToolBox::tab:hover {
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>799</width>
|
||||
<height>378</height>
|
||||
<height>370</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -241,7 +241,7 @@ QToolBox::tab:hover {
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>799</width>
|
||||
<height>378</height>
|
||||
<height>370</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -414,8 +414,8 @@ QToolBox::tab:hover {
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>381</width>
|
||||
<height>193</height>
|
||||
<width>799</width>
|
||||
<height>370</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -516,8 +516,8 @@ QToolBox::tab:hover {
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>340</width>
|
||||
<height>70</height>
|
||||
<width>799</width>
|
||||
<height>370</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -595,8 +595,8 @@ QToolBox::tab:hover {
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>384</width>
|
||||
<height>140</height>
|
||||
<width>799</width>
|
||||
<height>370</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -673,8 +673,8 @@ QToolBox::tab:hover {
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>479</width>
|
||||
<height>226</height>
|
||||
<width>799</width>
|
||||
<height>370</height>
|
||||
</rect>
|
||||
</property>
|
||||
<attribute name="label">
|
||||
@ -688,20 +688,36 @@ QToolBox::tab:hover {
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_remember_current_page">
|
||||
<property name="text">
|
||||
<string>Remember the &current page when quitting</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_copy_bookmarks_to_file">
|
||||
<property name="toolTip">
|
||||
<string>Keep a copy of all bookmarks/current page information inside the ebook file, so that you can share them by simply sending the ebook file itself. Currently only works with ebooks in the EPUB format.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Keep a copy of bookmarks/current page inside the ebook file, for easy sharing</string>
|
||||
<string>Keep a copy of &bookmarks/current page inside the ebook file, for easy sharing</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="search_online_url">
|
||||
<property name="toolTip">
|
||||
<string>Change the search engine used to perform online searches for selected text.
|
||||
You must enter the search URL for the search engine, with the placeholder
|
||||
{text}, which will be replaced by the selected text.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="hyphenate_default_lang">
|
||||
<property name="toolTip">
|
||||
<string>The default language to use for hyphenation rules. If the book does not specify a language, this will be used.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_remember_current_page">
|
||||
<property name="text">
|
||||
<string>Remember the &current page when quitting</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -719,7 +735,7 @@ QToolBox::tab:hover {
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<item row="8" column="0">
|
||||
<widget class="QPushButton" name="clear_search_history_button">
|
||||
<property name="text">
|
||||
<string>Clear search history</string>
|
||||
@ -736,13 +752,6 @@ QToolBox::tab:hover {
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="hyphenate_default_lang">
|
||||
<property name="toolTip">
|
||||
<string>The default language to use for hyphenation rules. If the book does not specify a language, this will be used.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_26">
|
||||
<property name="text">
|
||||
@ -753,16 +762,7 @@ QToolBox::tab:hover {
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="search_online_url">
|
||||
<property name="toolTip">
|
||||
<string>Change the search engine used to perform online searches for selected text.
|
||||
You must enter the search URL for the search engine, with the placeholder
|
||||
{text}, which will be replaced by the selected text.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<item row="9" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -775,6 +775,16 @@ You must enter the search URL for the search engine, with the placeholder
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_singleinstance">
|
||||
<property name="toolTip">
|
||||
<string>Normally, you can view multiple books in calibre, each in its own viewer window. With this option, if you attempt to view a second book, it will replace the previously opened book instead of using a new window.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Allow only a &single book to be viewed at a time (needs restart)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
@ -8,7 +8,7 @@ from collections import namedtuple
|
||||
|
||||
from PyQt5.Qt import (
|
||||
QApplication, Qt, QIcon, QTimer, QByteArray, QSize, QTime,
|
||||
QPropertyAnimation, QUrl, QInputDialog, QAction, QModelIndex)
|
||||
QPropertyAnimation, QUrl, QInputDialog, QAction, QModelIndex, pyqtSignal)
|
||||
|
||||
from calibre.gui2.viewer.ui import Main as MainWindow
|
||||
from calibre.gui2.viewer.toc import TOC
|
||||
@ -20,14 +20,17 @@ from calibre.ebooks.oeb.iterator.book import EbookIterator
|
||||
from calibre.constants import islinux, filesystem_encoding
|
||||
from calibre.utils.config import Config, StringConfig, JSONConfig
|
||||
from calibre.customize.ui import available_input_formats
|
||||
from calibre import as_unicode, force_unicode, isbytestring
|
||||
from calibre import as_unicode, force_unicode, isbytestring, prints
|
||||
from calibre.ptempfile import reset_base_dir
|
||||
from calibre.utils.ipc import viewer_socket_address, RC
|
||||
from calibre.utils.zipfile import BadZipfile
|
||||
from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1, get_lang
|
||||
|
||||
vprefs = JSONConfig('viewer')
|
||||
vprefs.defaults['singleinstance'] = False
|
||||
dprefs = JSONConfig('viewer_dictionaries')
|
||||
dprefs.defaults['word_lookups'] = {}
|
||||
singleinstance_name = 'calibre_viewer'
|
||||
|
||||
class Worker(Thread):
|
||||
|
||||
@ -70,6 +73,19 @@ def lookup_website(lang):
|
||||
wm = dprefs['word_lookups']
|
||||
return wm.get(lang, default_lookup_website(lang))
|
||||
|
||||
def listen(self):
|
||||
while True:
|
||||
try:
|
||||
conn = self.listener.accept()
|
||||
except Exception:
|
||||
break
|
||||
try:
|
||||
self.msg_from_anotherinstance.emit(conn.recv())
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
prints('Failed to read message from other instance with error: %s' % as_unicode(e))
|
||||
self.listener = None
|
||||
|
||||
class EbookViewer(MainWindow):
|
||||
|
||||
STATE_VERSION = 2
|
||||
@ -78,13 +94,20 @@ class EbookViewer(MainWindow):
|
||||
PAGED_MODE_TT = _('Switch to flow mode - where the text is not broken up '
|
||||
'into pages')
|
||||
AUTOSAVE_INTERVAL = 10 # seconds
|
||||
msg_from_anotherinstance = pyqtSignal(object)
|
||||
|
||||
def __init__(self, pathtoebook=None, debug_javascript=False, open_at=None,
|
||||
start_in_fullscreen=False, continue_reading=False):
|
||||
start_in_fullscreen=False, continue_reading=False, listener=None):
|
||||
MainWindow.__init__(self, debug_javascript)
|
||||
self.view.magnification_changed.connect(self.magnification_changed)
|
||||
self.closed = False
|
||||
self.show_toc_on_open = False
|
||||
self.listener = listener
|
||||
if listener is not None:
|
||||
t = Thread(name='ConnListener', target=listen, args=(self,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
self.msg_from_anotherinstance.connect(self.another_instance_wants_to_talk, type=Qt.QueuedConnection)
|
||||
self.current_book_has_toc = False
|
||||
self.iterator = None
|
||||
self.current_page = None
|
||||
@ -271,6 +294,8 @@ class EbookViewer(MainWindow):
|
||||
self.action_full_screen.trigger()
|
||||
return False
|
||||
self.save_state()
|
||||
if self.listener is not None:
|
||||
self.listener.close()
|
||||
return True
|
||||
|
||||
def quit(self):
|
||||
@ -858,6 +883,14 @@ class EbookViewer(MainWindow):
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def another_instance_wants_to_talk(self, msg):
|
||||
try:
|
||||
path, open_at = msg
|
||||
except Exception:
|
||||
return
|
||||
self.load_ebook(path, open_at=open_at)
|
||||
self.raise_()
|
||||
|
||||
def load_ebook(self, pathtoebook, open_at=None, reopen_at=None):
|
||||
if self.iterator is not None:
|
||||
self.save_current_position()
|
||||
@ -1080,6 +1113,40 @@ View an ebook.
|
||||
setup_gui_option_parser(parser)
|
||||
return parser
|
||||
|
||||
def create_listener():
|
||||
if islinux:
|
||||
from calibre.utils.ipc.server import LinuxListener as Listener
|
||||
else:
|
||||
from multiprocessing.connection import Listener
|
||||
return Listener(address=viewer_socket_address())
|
||||
|
||||
|
||||
def ensure_single_instance(args, open_at):
|
||||
try:
|
||||
from calibre.utils.lock import singleinstance
|
||||
si = singleinstance(singleinstance_name)
|
||||
except Exception:
|
||||
import traceback
|
||||
error_dialog(None, _('Cannot start viewer'), _(
|
||||
'Failed to start viewer, single instance locking failed. Click "Show Details" for more information'),
|
||||
det_msg=traceback.format_exc(), show=True)
|
||||
raise SystemExit(1)
|
||||
if not si:
|
||||
if len(args) > 1:
|
||||
t = RC(print_error=True, socket_address=viewer_socket_address())
|
||||
t.start()
|
||||
t.join(3.0)
|
||||
if t.is_alive() or t.conn is None:
|
||||
error_dialog(None, _('Connect talk to viewer'), _(
|
||||
'Unable to connect to existing viewer window, try restarting it.'), show=True)
|
||||
raise SystemExit(1)
|
||||
t.conn.send((os.path.abspath(args[1]), open_at))
|
||||
t.conn.close()
|
||||
prints('Opened book in existing viewer instance')
|
||||
raise SystemExit(0)
|
||||
listener = create_listener()
|
||||
return listener
|
||||
|
||||
|
||||
def main(args=sys.argv):
|
||||
# Ensure viewer can continue to function if GUI is closed
|
||||
@ -1089,15 +1156,25 @@ def main(args=sys.argv):
|
||||
parser = option_parser()
|
||||
opts, args = parser.parse_args(args)
|
||||
open_at = float(opts.open_at.replace(',', '.')) if opts.open_at else None
|
||||
listener = None
|
||||
override = 'calibre-ebook-viewer' if islinux else None
|
||||
app = Application(args, override_program_name=override, color_prefs=vprefs)
|
||||
app.load_builtin_fonts()
|
||||
app.setWindowIcon(QIcon(I('viewer.png')))
|
||||
QApplication.setOrganizationName(ORG_NAME)
|
||||
QApplication.setApplicationName(APP_UID)
|
||||
|
||||
if vprefs['singleinstance']:
|
||||
try:
|
||||
listener = ensure_single_instance(args, open_at)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_dialog(None, _('Failed to start viewer'), as_unicode(e), det_msg=traceback.format_exc(), show=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
main = EbookViewer(args[1] if len(args) > 1 else None,
|
||||
debug_javascript=opts.debug_javascript, open_at=open_at, continue_reading=opts.continue_reading,
|
||||
start_in_fullscreen=opts.full_screen)
|
||||
start_in_fullscreen=opts.full_screen, listener=listener)
|
||||
app.installEventFilter(main)
|
||||
# This is needed for paged mode. Without it, the first document that is
|
||||
# loaded will have extra blank space at the bottom, as
|
||||
@ -1115,4 +1192,3 @@ def main(args=sys.argv):
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
||||
|
@ -11,7 +11,7 @@ from threading import Thread
|
||||
|
||||
from calibre.constants import iswindows, get_windows_username, islinux
|
||||
|
||||
ADDRESS = None
|
||||
ADDRESS = VADDRESS = None
|
||||
|
||||
def eintr_retry_call(func, *args, **kwargs):
|
||||
while True:
|
||||
@ -48,10 +48,39 @@ def gui_socket_address():
|
||||
ADDRESS = os.path.join(tmp, user+'-calibre-gui.socket')
|
||||
return ADDRESS
|
||||
|
||||
|
||||
def viewer_socket_address():
|
||||
global VADDRESS
|
||||
if VADDRESS is None:
|
||||
if iswindows:
|
||||
VADDRESS = r'\\.\pipe\CalibreViewer'
|
||||
try:
|
||||
user = get_windows_username()
|
||||
except:
|
||||
user = None
|
||||
if user:
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
user = ascii_filename(user).replace(' ', '_')
|
||||
if user:
|
||||
VADDRESS += '-' + user[:100] + 'x'
|
||||
else:
|
||||
user = os.environ.get('USER', '')
|
||||
if not user:
|
||||
user = os.path.basename(os.path.expanduser('~'))
|
||||
if islinux:
|
||||
VADDRESS = (u'\0%s-calibre-viewer.socket' % user).encode('ascii')
|
||||
else:
|
||||
from tempfile import gettempdir
|
||||
tmp = gettempdir()
|
||||
VADDRESS = os.path.join(tmp, user+'-calibre-viewer.socket')
|
||||
return VADDRESS
|
||||
|
||||
|
||||
class RC(Thread):
|
||||
|
||||
def __init__(self, print_error=True):
|
||||
def __init__(self, print_error=True, socket_address=None):
|
||||
self.print_error = print_error
|
||||
self.socket_address = socket_address or gui_socket_address()
|
||||
Thread.__init__(self)
|
||||
self.conn = None
|
||||
self.daemon = True
|
||||
@ -60,11 +89,9 @@ class RC(Thread):
|
||||
from multiprocessing.connection import Client
|
||||
self.done = False
|
||||
try:
|
||||
self.conn = Client(gui_socket_address())
|
||||
self.conn = Client(self.socket_address)
|
||||
self.done = True
|
||||
except:
|
||||
if self.print_error:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user