-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
readerrolling.lua
2026 lines (1897 loc) · 90 KB
/
readerrolling.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
local BD = require("ui/bidi")
local Blitbuffer = require("ffi/blitbuffer")
local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Event = require("ui/event")
local InputContainer = require("ui/widget/container/inputcontainer")
local ProgressWidget = require("ui/widget/progresswidget")
local ReaderPanning = require("apps/reader/modules/readerpanning")
local Size = require("ui/size")
local UIManager = require("ui/uimanager")
local bit = require("bit")
local ffiutil = require("ffi/util")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local time = require("ui/time")
local _ = require("gettext")
local Input = Device.input
local Screen = Device.screen
local T = require("ffi/util").template
local band = bit.band
-- We need a small mmap'ped segment to exchange states with forked
-- subproceses doing background rerenderings.
-- Used as:
-- shared_state[0] = pid of current subprocess
-- shared_state[1] = 0 or 1, set by subprocess when rendering done, waiting to save cache
-- shared_state[2] = 0 or 1, set by main process when subprocess can go on saving cache
local ffi = require("ffi")
local shared_state_data = ffi.C.mmap(nil, 3*ffi.sizeof("uint32_t"), bit.bor(ffi.C.PROT_READ, ffi.C.PROT_WRITE),
bit.bor(ffi.C.MAP_SHARED, ffi.C.MAP_ANONYMOUS), -1, 0)
local shared_state = ffi.cast("uint32_t*", shared_state_data)
local koreader_pid = ffi.C.getpid()
--[[
Rolling is just like paging in page-based documents except that
sometimes (in scroll mode) there is no concept of page number to indicate
current progress.
There are three kind of progress measurements for credocuments.
1. page number (in page mode)
2. progress percentage (in scroll mode)
3. xpointer (in document dom structure)
We found that the first two measurements are not suitable for keeping a
record of the real progress. For example, when switching screen orientation
from portrait to landscape, or switching view mode from page to scroll, the
internal xpointer should not be used as the view dimen/mode is changed and
crengine's pagination mechanism will find a closest xpointer for the new view.
So if we change the screen orientation or view mode back, we cannot find the
original place since the internal xpointer is changed, which is counter-
intuitive as users didn't goto any other page.
The solution is that we keep a record of the internal xpointer and only change
it in explicit page turning. And use that xpointer for non-page-turning
rendering.
--]]
local ReaderRolling = InputContainer:extend{
pan_rate = 30, -- default 30 ops, will be adjusted in readerui
rendering_hash = 0,
current_pos = 0,
-- only used for page view mode
current_page = nil,
xpointer = nil,
panning_steps = ReaderPanning.panning_steps,
cre_top_bar_enabled = false,
-- With visible_pages=2, in 2-pages mode, ensure the first
-- page is always odd or even (odd is logical to avoid a
-- same page when turning first 2-pages set of document)
odd_or_even_first_page = 1, -- 1 = odd, 2 = even, nil or others = free
hide_nonlinear_flows = nil,
partial_rerendering = false,
nb_partial_rerenderings = 0,
rendering_state = nil,
RENDERING_STATE = {
FULLY_RENDERED = nil,
PARTIALLY_RERENDERED = 1,
FULL_RENDERING_IN_BACKGROUND = 2,
FULL_RENDERING_READY = 3,
RELOADING_DOCUMENT = 4,
DO_RELOAD_DOCUMENT = 5,
},
mark_func = nil,
unmark_func = nil,
_stepRerenderingAutomation = nil,
}
function ReaderRolling:init()
self:registerKeyEvents()
self.pan_interval = time.s(1 / self.pan_rate)
table.insert(self.ui.postInitCallback, function()
self.rendering_hash = self.ui.document:getDocumentRenderingHash(true)
self.ui.document:_readMetadata()
if self.ui.document:hasCacheFile() and not self.ui.document:isCacheFileStale() then
-- We loaded from a valid cache file: remember its hash. It may allow not
-- having to do any background rerendering if the user somehow reverted
-- some setting changes before any background rerendering had completed
-- (ie. with autorotation, transitionning from portrait to landscape for
-- a few seconds, to then end up back in portrait).
self.valid_cache_rendering_hash = self.ui.document:getDocumentRenderingHash(false)
end
end)
table.insert(self.ui.postReaderCallback, function()
self:updatePos()
-- Disable crengine internal history, with required redraw
self.ui.document:enableInternalHistory(false)
self:onRedrawCurrentView()
end)
self.ui.menu:registerToMainMenu(self)
self.batched_update_count = 0
-- delegate gesture listener to readerui, NOP our own
self.ges_events = nil
end
function ReaderRolling:onGesture() end
function ReaderRolling:registerKeyEvents()
if Device:hasKeys() then
self.key_events.GotoNextView = {
{ { "RPgFwd", "LPgFwd", "Right" } },
event = "GotoViewRel",
args = 1,
}
self.key_events.GotoPrevView = {
{ { "RPgBack", "LPgBack", "Left" } },
event = "GotoViewRel",
args = -1,
}
end
if Device:hasDPad() then
self.key_events.MoveUp = {
{ "Up" },
event = "Panning",
args = {0, -1},
}
self.key_events.MoveDown = {
{ "Down" },
event = "Panning",
args = {0, 1},
}
end
if Device:hasKeyboard() then
self.key_events.GotoFirst = {
{ "1" },
event = "GotoPercent",
args = 0,
}
self.key_events.Goto11 = {
{ "2" },
event = "GotoPercent",
args = 11,
}
self.key_events.Goto22 = {
{ "3" },
event = "GotoPercent",
args = 22,
}
self.key_events.Goto33 = {
{ "4" },
event = "GotoPercent",
args = 33,
}
self.key_events.Goto44 = {
{ "5" },
event = "GotoPercent",
args = 44,
}
self.key_events.Goto55 = {
{ "6" },
event = "GotoPercent",
args = 55,
}
self.key_events.Goto66 = {
{ "7" },
event = "GotoPercent",
args = 66,
}
self.key_events.Goto77 = {
{ "8" },
event = "GotoPercent",
args = 77,
}
self.key_events.Goto88 = {
{ "9" },
event = "GotoPercent",
args = 88,
}
self.key_events.GotoLast = {
{ "0" },
event = "GotoPercent",
args = 100,
}
end
end
ReaderRolling.onPhysicalKeyboardConnected = ReaderRolling.registerKeyEvents
function ReaderRolling:onReadSettings(config)
-- 20180503: some fix in crengine has changed the way the DOM is built
-- for HTML documents and may make XPATHs obtained from previous version
-- invalid.
-- We may request the previous (buggy) behaviour though, which we do
-- if we use a DocSetting previously made that may contain bookmarks
-- and highlights with old XPATHs.
-- (EPUB will use the same correct DOM code no matter what DOM version
-- we request here.)
if config:hasNot("cre_dom_version") then
-- Not previously set, guess which DOM version to use
if config:has("last_xpointer") then
-- We have a last_xpointer: this book was previously opened
-- with possibly a very old version: request the oldest
config:saveSetting("cre_dom_version", self.ui.document:getOldestDomVersion())
else
-- No previous xpointer: book never opened (or sidecar file
-- purged): we can use the latest DOM version
config:saveSetting("cre_dom_version", self.ui.document:getLatestDomVersion())
end
end
self.ui.document:requestDomVersion(config:readSetting("cre_dom_version"))
-- If we're using a DOM version without normalized XPointers, some stuff
-- may need tweaking:
local cre = require("document/credocument"):engineInit()
if config:readSetting("cre_dom_version") < cre.getDomVersionWithNormalizedXPointers() then
-- Show some warning when styles "display:" have changed that
-- bookmarks may break
self.using_non_normalized_xpointers = true
-- Also tell ReaderTypeset, which ensures block rendering mode,
-- that we'd rather have some of its BLOCK_RENDERING_FLAGS disabled
-- if an old DOM version is requested, as some flags may "box"
-- (into inserted internal elements) long fragment of text,
-- which may break previous highlights.
self.ui.typeset:ensureSanerBlockRenderingFlags()
-- And check if we can migrate to a newest DOM version after
-- the book is loaded (unless the user told us not to).
if config:nilOrFalse("cre_keep_old_dom_version") then
self.ui:registerPostReadyCallback(function()
self:checkXPointersAndProposeDOMVersionUpgrade()
end)
end
end
local last_xp = config:readSetting("last_xpointer")
local last_per = config:readSetting("last_percent")
if last_xp then
self.xpointer = last_xp
self.setupXpointer = function()
self:_gotoXPointer(self.xpointer)
-- we have to do a real jump in self.ui.document._document to
-- update status information in CREngine.
self.ui.document:gotoXPointer(self.xpointer)
end
-- we read last_percent just for backward compatibility
--- @fixme remove this branch with migration script
elseif last_per then
self.setupXpointer = function()
self:_gotoPercent(last_per * 100)
-- _gotoPercent calls _gotoPos, which only updates self.current_pos
-- and self.view.
-- we need to do a real pos change in self.ui.document._document
-- to update status information in CREngine.
self.ui.document:gotoPos(self.current_pos)
-- _gotoPercent already calls gotoPos, so no need to emit
-- PosUpdate event in scroll mode
if self.view.view_mode == "page" then
self.ui:handleEvent(
Event:new("PageUpdate", self.ui.document:getCurrentPage()))
end
self.xpointer = self.ui.document:getXPointer()
end
else
self.setupXpointer = function()
self.xpointer = self.ui.document:getXPointer()
if self.view.view_mode == "page" then
self.ui:handleEvent(Event:new("PageUpdate", self.ui.document:getNextPage(0)))
end
end
end
-- self.configurable.visible_pages may not be the current nb of visible pages
-- as crengine may decide to not ensure that in some conditions.
-- It's the one we got from settings, the one the user has decided on
-- with config toggle, and the one that we will save for next load.
-- Use self.ui.document:getVisiblePageCount() to get the current
-- crengine used value.
self.ui.document:setVisiblePageCount(self.configurable.visible_pages)
if config:has("hide_nonlinear_flows") then
self.hide_nonlinear_flows = config:isTrue("hide_nonlinear_flows")
else
self.hide_nonlinear_flows = G_reader_settings:isTrue("hide_nonlinear_flows")
end
self.ui.document:setHideNonlinearFlows(self.hide_nonlinear_flows)
-- Will be activated on ReaderReady
if config:has("partial_rerendering") then
self.partial_rerendering = config:isTrue("partial_rerendering")
else
self.partial_rerendering = G_reader_settings:nilOrTrue("cre_partial_rerendering")
end
-- Set a callback to allow showing load and rendering progress
-- (this callback will be cleaned up by cre.cpp closeDocument(),
-- no need to handle it in :onCloseDocument() here.)
self.ui.document:setCallback(function(...)
-- Catch and log any error happening in handleCallback(),
-- as otherwise it would just silently abort (but beware
-- having errors, this may flood crash.log)
local ok, err = xpcall(self.handleEngineCallback, debug.traceback, self, ...)
if not ok then
logger.warn("cre callback() error:", err)
end
end)
end
function ReaderRolling:onCloseDocument()
self:tearDownRerenderingAutomation()
-- Unschedule anything that might still somehow be...
if self.mark_func then
UIManager:unschedule(self.mark_func)
end
if self.unmark_func then
UIManager:unschedule(self.unmark_func)
end
UIManager:unschedule(self.onCheckDomStyleCoherence)
UIManager:unschedule(self.onUpdatePos)
self.current_header_height = nil -- show unload progress bar at top
-- we cannot do it in onSaveSettings() because getLastPercent() uses self.ui.document
self.ui.doc_settings:saveSetting("percent_finished", self:getLastPercent())
local cache_file_path = self.ui.document:getCacheFilePath() -- nil if no cache file
self.ui.doc_settings:saveSetting("cache_file_path", cache_file_path)
if self.ui.document:hasCacheFile() then
-- also checks if DOM is coherent with styles; if not, invalidate the
-- cache, so a new DOM is built on next opening
-- Don't check if we are reloading from a cache built in background: if
-- incoherent, the user will get the popup after the re-open, and can
-- decide to reload, or just revert his changes and avoid the reload.
if self.ui.document:isBuiltDomStale() and self.rendering_state ~= self.RENDERING_STATE.DO_RELOAD_DOCUMENT then
logger.dbg("cre DOM may not be in sync with styles, invalidating cache file for a full reload at next opening")
self.ui.document:invalidateCacheFile()
end
end
logger.dbg("cre cache used:", cache_file_path or "none")
-- Unknown elements and attributes, uncomment if needed for debugging:
-- local elements, attributes, namespaces = self.ui.document:getUnknownEntities()
-- if elements ~= "" then logger.info("cre unknown elements: ", elements) end
-- if attributes ~= "" then logger.info("cre unknown attributes: ", attributes) end
-- if namespaces ~= "" then logger.info("cre unknown namespaces: ", namespaces) end
end
function ReaderRolling:onCheckDomStyleCoherence()
if self.ui.document and self.ui.document:isBuiltDomStale() then
local has_bookmarks_warn_txt = ""
-- When using an older DOM version, bookmarks may break
if self.using_non_normalized_xpointers and self.ui.bookmark:hasBookmarks() then
has_bookmarks_warn_txt = _("\nNote that this change in styles may render your bookmarks or highlights no more valid.\nIf some of them do not show anymore, you can just revert the change you just made to have them shown again.\n\n")
end
UIManager:show(ConfirmBox:new{
text = T(_("Styles have changed in such a way that fully reloading the document may be needed for a correct rendering.\n%1Do you want to reload the document?"), has_bookmarks_warn_txt),
ok_callback = function()
-- Allow for ConfirmBox to be closed before showing
-- "Opening file" InfoMessage
UIManager:scheduleIn(0.5, function()
-- And check we haven't quit reader in these 0.5s
if self.ui.document then
self.ui:reloadDocument()
end
end)
end,
})
end
end
function ReaderRolling:onSaveSettings()
self.ui.doc_settings:delSetting("last_percent") -- deprecated
self.ui.doc_settings:saveSetting("last_xpointer", self.xpointer)
self.ui.doc_settings:saveSetting("hide_nonlinear_flows", self.hide_nonlinear_flows)
self.ui.doc_settings:saveSetting("partial_rerendering", self.partial_rerendering)
end
function ReaderRolling:onReaderReady()
self:setupTouchZones()
if self.hide_nonlinear_flows then
self.ui.document:cacheFlows()
end
self.setupXpointer()
if self.partial_rerendering then
UIManager:nextTick(function()
if self.ui.document then -- (could have disappeared with unit tests)
self.ui.document:enablePartialRerendering(true)
end
end)
end
end
function ReaderRolling:setupTouchZones()
if not Device:isTouchDevice() then return end
local forward_zone, backward_zone = self.view:getTapZones()
self.ui:registerTouchZones({
{
id = "tap_forward",
ges = "tap",
screen_zone = forward_zone,
handler = function()
if G_reader_settings:nilOrFalse("page_turns_disable_tap") then
return self:onGotoViewRel(1)
end
end,
},
{
id = "tap_backward",
ges = "tap",
screen_zone = backward_zone,
handler = function()
if G_reader_settings:nilOrFalse("page_turns_disable_tap") then
return self:onGotoViewRel(-1)
end
end,
},
{
id = "rolling_swipe",
ges = "swipe",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges) return self:onSwipe(nil, ges) end,
},
{
id = "rolling_pan",
ges = "pan",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges) return self:onPan(nil, ges) end,
},
{
id = "rolling_pan_release",
ges = "pan_release",
screen_zone = {
ratio_x = 0, ratio_y = 0, ratio_w = 1, ratio_h = 1,
},
handler = function(ges) return self:onPanRelease(nil, ges) end,
},
})
end
function ReaderRolling:getLastProgress()
return self.xpointer
end
function ReaderRolling:addToMainMenu(menu_items)
if self.ui.document:hasNonLinearFlows() then
local hide_nonlinear_text = _("When hide non-linear fragments is enabled, any non-linear fragments will be hidden from the normal page flow. Such fragments will always remain accessible through links, the table of contents and the 'Go to' dialog. This only works in single-page mode.")
menu_items.hide_nonlinear_flows = {
text = _("Hide non-linear fragments"),
enabled_func = function()
-- Custom hidden flows have precedence over publisher hidden non-linear fragments
return self.view.view_mode == "page" and self.ui.document:getVisiblePageCount() == 1
and not self.ui.handmade:isHandmadeHiddenFlowsEnabled()
end,
checked_func = function() return self.hide_nonlinear_flows end,
callback = function()
self:onToggleHideNonlinear()
end,
hold_callback = function()
UIManager:show(ConfirmBox:new{
text = T(
hide_nonlinear_text .. "\n\n" .. _("Set default hide non-linear fragments to %1?"),
self.hide_nonlinear_flows and _("enabled") or _("disabled")
),
ok_callback = function()
G_reader_settings:saveSetting("hide_nonlinear_flows", self.hide_nonlinear_flows)
end,
})
end,
help_text = hide_nonlinear_text,
}
end
menu_items.partial_rerendering = {
text = _("Enable partial renderings"),
enabled_func = function()
return self.ui.document:canBePartiallyRerendered() == true
end,
checked_func = function()
return self.ui.document:isPartialRerenderingEnabled() == true
end,
callback = function()
if self.ui.document:isPartialRerenderingEnabled() then
-- (Don't disable it if we are currently in a rerendering automation)
if not self.rendering_state then
self.partial_rerendering = false
if self.ui.document:enablePartialRerendering(false) then
-- Disabling returns true when some partial rerenderings had been done.
-- A full rerendering is needed to have a properly rendered document.
self:onUpdatePos()
end
end
else
self.ui.document:enablePartialRerendering(true)
self.partial_rerendering = self.ui.document:isPartialRerenderingEnabled()
end
end,
hold_callback = function()
local cre_partial_rerendering = G_reader_settings:nilOrTrue("cre_partial_rerendering")
local text = _([[
With EPUB documents (having multiple fragments), text appearance adjustments can be made quicker by only rendering the current chapter.
After such partial renderings, the book and KOReader are in a degraded state: you can turn pages, but some info and features may be broken or disabled (ie. footer info, ToC, statistics…).
To get back to a sane state, a full rendering will happen in the background, get cached, and the document will be seamlessly reloaded after a brief period of inactivity.]])
if cre_partial_rerendering then
text = text .. "\n\n" .. _("The current default (★) is to enable partial renderings when possible.")
else
text = text .. "\n\n" .. _("The current default (★) is to always do a full rendering.")
end
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
UIManager:show(MultiConfirmBox:new{
text = text,
-- This text is a bit long, and MultiConfirmBox currently doesn't adjust the
-- font size and may overflow the screen height: use a smaller font size
face = require("ui/font"):getFace("infofont", 20),
icon = "cre.render.partial",
choice1_text_func = function()
return cre_partial_rerendering and _("Disable") or _("Disable (★)")
end,
choice1_callback = function()
G_reader_settings:makeFalse("cre_partial_rerendering")
end,
choice2_text_func = function()
return cre_partial_rerendering and _("Enable (★)") or _("Enable")
end,
choice2_callback = function()
G_reader_settings:makeTrue("cre_partial_rerendering")
end,
})
end,
}
end
function ReaderRolling:getLastPercent()
if self.view.view_mode == "page" then
return self.current_page / self.ui.document.info.number_of_pages
else
--- @fixme the calculated percent is not accurate in "scroll" mode.
return self.ui.document:getPosFromXPointer(
self.ui.document:getXPointer()) / self.ui.document.info.doc_height
end
end
function ReaderRolling:onScrollSettingsUpdated(scroll_method, inertial_scroll_enabled, scroll_activation_delay_ms)
self.scroll_method = scroll_method
self.scroll_activation_delay = time.ms(scroll_activation_delay_ms)
if inertial_scroll_enabled then
self.ui.scrolling:setInertialScrollCallbacks(
function(distance) -- do_scroll_callback
if not self.ui.document then
return false
end
UIManager.currently_scrolling = true
local prev_pos = self.current_pos
self:_gotoPos(prev_pos + distance)
return self.current_pos ~= prev_pos
end,
function() -- scroll_done_callback
UIManager.currently_scrolling = false
if self.ui.document then
self.xpointer = self.ui.document:getXPointer()
end
UIManager:setDirty(self.view.dialog, "partial")
end
)
else
self.ui.scrolling:setInertialScrollCallbacks(nil, nil)
end
end
function ReaderRolling:onSwipe(_, ges)
if self._pan_has_scrolled then
-- We did some panning but released after a short amount of time,
-- so this gesture ended up being a Swipe - and this swipe was
-- not handled by the other modules (so, not opening the menus).
-- Do as :onPanRelese() and ignore this swipe.
self:onPanRelease() -- no arg, so we know there we come from here
return true
else
self._pan_started = false
UIManager.currently_scrolling = false
end
local direction = BD.flipDirectionIfMirroredUILayout(ges.direction)
if direction == "west" then
if G_reader_settings:nilOrFalse("page_turns_disable_swipe") then
if self.view.inverse_reading_order then
self:onGotoViewRel(-1)
else
self:onGotoViewRel(1)
end
return true
end
elseif direction == "east" then
if G_reader_settings:nilOrFalse("page_turns_disable_swipe") then
if self.view.inverse_reading_order then
self:onGotoViewRel(1)
else
self:onGotoViewRel(-1)
end
return true
end
end
end
function ReaderRolling:onPan(_, ges)
if ges.direction == "north" or ges.direction == "south" then
if ges.mousewheel_direction and self.view.view_mode == "page" then
-- Mouse wheel generates a Pan event: in page mode, move one
-- page per event. Scroll mode is handled in the 'else' branch
-- and use the wheeled distance.
UIManager:broadcastEvent(Event:new("GotoViewRel", -1 * ges.mousewheel_direction))
elseif self.view.view_mode == "scroll" then
if not self._pan_started then
self._pan_started = true
-- Re-init state variables
self._pan_has_scrolled = false
self._pan_prev_relative_y = 0
self._pan_to_scroll_later = 0
self._pan_real_last_time = 0
if ges.mousewheel_direction then
self._pan_activation_time = false
else
self._pan_activation_time = ges.time + self.scroll_activation_delay
end
-- We will restore the previous position if this pan
-- ends up being a swipe or a multiswipe
self._pan_pos_at_pan_start = self.current_pos
end
local scroll_now = false
if self._pan_activation_time and ges.time >= self._pan_activation_time then
self._pan_activation_time = false -- We can go on, no need to check again
end
if not self._pan_activation_time and ges.time - self._pan_real_last_time >= self.pan_interval then
scroll_now = true
self._pan_real_last_time = ges.time
end
local scroll_dist = 0
if self.scroll_method == self.ui.scrolling.SCROLL_METHOD_CLASSIC then
-- Scroll by the distance the finger moved since last pan event,
-- having the document follows the finger
scroll_dist = self._pan_prev_relative_y - ges.relative.y
self._pan_prev_relative_y = ges.relative.y
if not self._pan_has_scrolled then
-- Avoid checking this for each pan, no need once we have scrolled
if self.ui.scrolling:cancelInertialScroll() or self.ui.scrolling:cancelledByTouch() then
-- If this pan or its initial touch did cancel some inertial scrolling,
-- ignore activation delay to allow continuous scrolling
self._pan_activation_time = false
scroll_now = true
self._pan_real_last_time = ges.time
end
end
self.ui.scrolling:accountManualScroll(scroll_dist, ges.time)
elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_TURBO then
-- Legacy scrolling "buggy" behaviour, that can actually be nice
-- Scroll by the distance from the initial finger position, this distance
-- controlling the speed of the scrolling)
if scroll_now then
scroll_dist = -ges.relative.y
end
-- We don't accumulate in _pan_to_scroll_later
elseif self.scroll_method == self.ui.scrolling.SCROLL_METHOD_ON_RELEASE then
self._pan_to_scroll_later = -ges.relative.y
if scroll_now then
self._pan_has_scrolled = true -- so we really apply it later
end
scroll_dist = 0
scroll_now = false
end
if scroll_now then
local dist = self._pan_to_scroll_later + scroll_dist
self._pan_to_scroll_later = 0
if dist ~= 0 then
self._pan_has_scrolled = true
UIManager.currently_scrolling = true
self:_gotoPos(self.current_pos + dist)
-- (We'll update self.xpointer only when done moving, at
-- release/swipe time as it might be expensive)
end
else
self._pan_to_scroll_later = self._pan_to_scroll_later + scroll_dist
end
end
end
return true
end
function ReaderRolling:onPanRelease(_, ges)
if self._pan_has_scrolled and self._pan_to_scroll_later ~= 0 then
self:_gotoPos(self.current_pos + self._pan_to_scroll_later)
end
self._pan_started = false
UIManager.currently_scrolling = false
if self._pan_has_scrolled then
self._pan_has_scrolled = false
self.xpointer = self.ui.document:getXPointer()
-- Don't do any inertial scrolling if pan events come from
-- a mousewheel (which may have itself some inertia)
if (ges and ges.from_mousewheel) or not self.ui.scrolling:startInertialScroll() then
UIManager:setDirty(self.view.dialog, "partial")
end
end
end
function ReaderRolling:onHandledAsSwipe()
if self._pan_started then
-- Restore original position as this pan we've started handling
-- has ended up being a multiswipe or handled as a swipe to open
-- top or bottom menus
self:_gotoPos(self._pan_pos_at_pan_start)
self._pan_started = false
self._pan_has_scrolled = false
UIManager.currently_scrolling = false
-- No specific refresh: the swipe/multiswipe might show other stuff,
-- and we'd want to avoid a flashing refresh
end
return true
end
function ReaderRolling:onPosUpdate(new_pos)
self.current_pos = new_pos
self:updateBatteryState()
end
function ReaderRolling:onPageUpdate(new_page)
self.current_page = new_page
self:updateBatteryState()
end
function ReaderRolling:onResume()
self:updateBatteryState()
end
function ReaderRolling:onGotoNextChapter()
local visible_page_count = self.ui.document:getVisiblePageNumberCount()
local pageno = self.current_page + (visible_page_count > 1 and 1 or 0)
local new_page
if self.ui.document:hasHiddenFlows() then
-- Find next chapter start
new_page = self.ui.document:getNextPage(pageno)
while new_page > 0 do
if self.ui.toc:isChapterStart(new_page) then break end
new_page = self.ui.document:getNextPage(new_page)
end
else
new_page = self.ui.toc:getNextChapter(pageno) or 0
end
if new_page > 0 then
self.ui.link:addCurrentLocationToStack()
self:onGotoPage(new_page)
end
return true
end
function ReaderRolling:onGotoPrevChapter()
local pageno = self.current_page
local new_page
if self.ui.document:hasHiddenFlows() then
-- Find previous chapter start
new_page = self.ui.document:getPrevPage(pageno)
while new_page > 0 do
if self.ui.toc:isChapterStart(new_page) then break end
new_page = self.ui.document:getPrevPage(new_page)
end
else
new_page = self.ui.toc:getPreviousChapter(pageno) or 0
end
if new_page > 0 then
self.ui.link:addCurrentLocationToStack()
self:onGotoPage(new_page)
end
return true
end
function ReaderRolling:onNotCharging()
self:updateBatteryState()
end
function ReaderRolling:onGotoPercent(percent)
logger.dbg("goto document offset in percent:", percent)
self:_gotoPercent(percent)
self.xpointer = self.ui.document:getXPointer()
return true
end
function ReaderRolling:onGotoPage(number)
if number then
self:_gotoPage(number)
end
self.xpointer = self.ui.document:getXPointer()
return true
end
function ReaderRolling:onGotoRelativePage(number)
if number then
self:_gotoPage(self.current_page + number)
end
self.xpointer = self.ui.document:getXPointer()
return true
end
function ReaderRolling:onGotoXPointer(xp, marker_xp)
if self.mark_func then
-- Unschedule previous marker as it's no longer accurate.
UIManager:unschedule(self.mark_func)
self.mark_func = nil
end
if self.unmark_func then
-- execute scheduled unmark now to clean previous marker
UIManager:unschedule(self.unmark_func)
self.unmark_func()
self.unmark_func = nil
end
self:_gotoXPointer(xp)
self.xpointer = xp
-- Allow tweaking this marker behaviour with a manual setting:
-- followed_link_marker = false: no marker shown
-- followed_link_marker = true: maker shown and not auto removed
-- followed_link_marker = <number>: removed after <number> seconds
-- (no real need for a menu item, the default is the finest)
local marker_setting
if G_reader_settings:has("followed_link_marker") then
marker_setting = G_reader_settings:readSetting("followed_link_marker")
else
marker_setting = 1 -- default is: shown and removed after 1 second
end
if marker_xp and marker_setting then
-- Show a mark on left side of screen to give a visual feedback of
-- where xpointer target is (and remove if after 1s)
local screen_y, screen_x = self.ui.document:getScreenPositionFromXPointer(marker_xp)
local doc_margins = self.ui.document:getPageMargins()
local marker_h = Screen:scaleBySize(self.configurable.font_size * 1.1 * self.configurable.line_spacing * (1/100))
-- Make it 4/5 of left margin wide (and bigger when huge margin)
local marker_w = math.floor(math.max(doc_margins["left"] - Screen:scaleBySize(5), doc_margins["left"] * 4/5))
if self.ui.document:getVisiblePageCount() > 1 then -- 2-pages mode
if screen_x < Screen:getWidth() / 2 then -- On left page
if BD.mirroredUILayout() then
-- In the middle margin, on the right of text
-- Same trick as below, assuming page2_x is equal to page 1 right x
screen_x = math.floor(Screen:getWidth() * 0.5)
local page2_x = self.ui.document:getPageOffsetX(self.ui.document:getCurrentPage(true)+1)
marker_w = page2_x + marker_w - screen_x
screen_x = screen_x - marker_w
else
screen_x = 0 -- In left page left margin
end
else -- On right page
if BD.mirroredUILayout() then
screen_x = Screen:getWidth() - marker_w -- In right page right margin
else
-- In the middle margin, on the left of text
-- This is a bit tricky with how the middle margin is sized
-- by crengine (see LVDocView::updateLayout() in lvdocview.cpp)
screen_x = math.floor(Screen:getWidth() * 0.5)
local page2_x = self.ui.document:getPageOffsetX(self.ui.document:getCurrentPage(true)+1)
marker_w = page2_x + marker_w - screen_x
end
end
else -- 1-page mode
if BD.mirroredUILayout() then
screen_x = Screen:getWidth() - marker_w -- In right margin
else
screen_x = 0 -- In left margin
end
end
self.mark_func = function()
self.mark_func = nil
local delayed_unmark = type(marker_setting) == "number"
if delayed_unmark then -- we'll have to remove the marker
-- We remember the original content that was where we are going
-- to draw the marker.
-- It's usually some white margin, so we could just draw a white
-- rectangle to unmark it; but it might not always be just white
-- margin: when we're in dual page mode and crengine has drawn a
-- vertical pages separator - or if we have had crengine draw
-- some backgroud texture with credocument:setBackgroundImage().
if self.mark_orig_content_bb then
-- be sure we don't leak memory if a previous one is still
-- hanging around
self.mark_orig_content_bb:free()
self.mark_orig_content_bb = nil
end
self.mark_orig_content_bb = Blitbuffer.new(marker_w, marker_h, Screen.bb:getType())
self.mark_orig_content_bb:blitFrom(Screen.bb, 0, 0, screen_x, screen_y, marker_w, marker_h)
end
-- Paint directly to the screen and force a regional refresh
Screen.bb:paintRect(screen_x, screen_y, marker_w, marker_h, Blitbuffer.COLOR_BLACK)
Screen:refreshFast(screen_x, screen_y, marker_w, marker_h)
if delayed_unmark then
self.unmark_func = function()
self.unmark_func = nil
-- UIManager:setDirty(self.view.dialog, "ui", Geom:new({x=0, y=screen_y, w=marker_w, h=marker_h}))
-- No need to use setDirty (which would ask crengine to
-- re-render the page, which may take a few seconds on big
-- documents). We just restore what was there by painting
-- it directly to screen and triggering a regional refresh.
if self.mark_orig_content_bb then
Screen.bb:blitFrom(self.mark_orig_content_bb, screen_x, screen_y, 0, 0, marker_w, marker_h)
Screen:refreshUI(screen_x, screen_y, marker_w, marker_h)
self.mark_orig_content_bb:free()
self.mark_orig_content_bb = nil
end
end
UIManager:scheduleIn(marker_setting, self.unmark_func)
end
end
UIManager:scheduleIn(0.5, self.mark_func)
end
return true
end
function ReaderRolling:getBookLocation()
return self.xpointer
end
function ReaderRolling:onRestoreBookLocation(saved_location)
return self:onGotoXPointer(saved_location.xpointer, saved_location.marker_xpointer)
end
function ReaderRolling:onGotoViewRel(diff)
logger.dbg("goto relative screen:", diff, "in mode:", self.view.view_mode)
if self.view.view_mode == "scroll" then
local footer_height = ((self.view.footer_visible and not self.view.footer.settings.reclaim_height) and 1 or 0) * self.view.footer:getHeight()
local page_visible_height = self.ui.dimen.h - footer_height
local pan_diff = diff * page_visible_height
if self.view.page_overlap_enable then
local overlap_lines = G_reader_settings:readSetting("copt_overlap_lines") or 1
local overlap_h = Screen:scaleBySize(self.configurable.font_size * 1.1 * self.configurable.line_spacing * (1/100)) * overlap_lines
if pan_diff > overlap_h then
pan_diff = pan_diff - overlap_h
elseif pan_diff < -overlap_h then
pan_diff = pan_diff + overlap_h
end
end
local old_pos = self.current_pos
-- Only draw dim area when we moved a whole page (not when smaller scroll with Pan)
local do_dim_area = math.abs(diff) == 1
self:_gotoPos(self.current_pos + pan_diff, do_dim_area)
if diff > 0 and old_pos == self.current_pos then
self.ui:handleEvent(Event:new("EndOfBook"))
end
elseif self.view.view_mode == "page" then
local page_count = self.ui.document:getVisiblePageNumberCount()
local old_page = self.current_page
-- we're in paged mode, so round up
if diff > 0 then
diff = math.ceil(diff)
else
diff = math.floor(diff)
end
local new_page = self.current_page
if self.ui.document:hasHiddenFlows() then
local test_page
for i=1, math.abs(diff*page_count) do
if diff > 0 then
test_page = self.ui.document:getNextPage(new_page)
else
test_page = self.ui.document:getPrevPage(new_page)
end
if test_page > 0 then
new_page = test_page
end
end
else
new_page = new_page + diff*page_count
end
self:_gotoPage(new_page)
if diff > 0 and old_page == self.current_page then
self.ui:handleEvent(Event:new("EndOfBook"))
end
end
if self.ui.document ~= nil then
self.xpointer = self.ui.document:getXPointer()
end
return true
end
function ReaderRolling:onPanning(args, _)
local _, dy = unpack(args)
if self.view.view_mode ~= "scroll" then
UIManager:broadcastEvent(Event:new("GotoViewRel", dy))
return
end
self:_gotoPos(self.current_pos + dy * self.panning_steps.normal)
self.xpointer = self.ui.document:getXPointer()
return true
end