-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathgeneral-code-overview.txt
982 lines (774 loc) · 45.2 KB
/
general-code-overview.txt
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
Table of contents:
a) How Quick Picto Viewer initializes
b) How a files list index is created
c) Files index filtering
d) How an image is loaded and displayed on screen
e) General considerations
f) How slideshows happen
g) Windows, AHK GUIs, Menus, Toolbar
h) Input responses
i) Image live editing
j) How to enable alpha masking for an image editing live tool
k) How vector shapes drawing works
l) How image duplicates identification works (soon)
m) How to add a new panel/tool (soon)
Deniere mise à jour: dimanche 24 mars 2024
=====================================================================
-) Introduction
The code of QPV is poorly structured and organized. It relies on
many global variables... and is hard to follow. This is the result
of me having no prior experience in coding such large projects.
It is a miracle it all works... ^_^ I should have used classes
to define objects such as «file list», «indexed item», «viewport
image», et cetera, and have constructors for the different kind of
tools. This document is meant to help people get their mittens into
the code of Quick Picto Viewer by offering pointers to the main
functions and how things connect / work.
Another big problem of the project is the reliance on GDI+. This
hinders the performance of the application. I should have used
Direct2D and shaders. In addition to this, I find it likely there
are better image libraries than FreeImage, eg., OpenCV perhaps.
And finally, AHK v1.1 is fundamentally not a wise choice to be
coding such a big application, even if I would have used classes
to better structure the code. AHK's loops are utterly slow,
rendering any processing of large amounts of data... a pain.
I circumvented this by creating my own DLL in C++ which I rely
on to process data.
QPV v6.0 is the last major development push that I plan. This
version brings full support for images above 536 megapixels
(the limit of GDI+), through FreeImage. Most of the image
editing tools will work with such images - through my own DLL:
"qpvmain.dll".
I use a custom build of the FreeImage.DLL. Because the developer
behind Freeimage rarely updates the library, I decided to make my
own version. It has multiple fixes and improvements. It can be
found on Github:
https://github.com/marius-sucan/FreeImage-library/tree/qpv
a) How Quick Picto Viewer initializes
=====================================================================
QPV checks in the registry if another instance started before,
if it did less than 600 miliseconds ago, it exits; this is meant
to prevent excessive spawn of multiple instances.
It first loads settings or stores default settings [if first start].
- loadMainSettings() or writeMainSettings(). These two functions
load and write only the core settings in an INI: quick-picto-viewer.ini.
Each panel [or tool] within QPV loads and stores settings that
pertain only to that specific, in the registry. Such functions are
named using this pattern: "ReadSettings*Panel".
If "Running" is set to 2 in the Registry by, supposedly, another
QPV instance, it will start as an aid for batch processing
image files. When it finishes, it exits. If it is not set to 2,
it continues the auto-exec part.
QPV initializes the external UI thread and various GDI+ objects
related to it. If something fails, QPV will ask the user to
attempt again to initialise or exit.
intializeWithGivenArguments() function processes all the command
line given arguments. The user can set global variables using
set_GlobalVar and invoke functions that take no parameters with
call_funcName(). Examples:
qpv.exe "fd=C:\example folder\tempus"
Add a pipe "|" after equal "=" to have images loaded recursively.
qpv.exe call_ToggleThumbsMode() "fd=C:\folder\tempus"
qpv.exe set_IMGresizingMode=3 set_vpIMGrotation=45 "C:\folder\this-image.png"
If an image file is given as command line argument, it is
opened with OpenArgFile(). If a .SLD or a .SLDB file is fed,
OpenSLD() function is used to load the files index. Folders
are opened using OpenFolders().
If no argument given, it draws a random pattern image using
drawWelcomeImg(). This function can generate up to 10 distinct
patterns, each randomized in its own way.
On 32 bits, the following settings are in effect by default:
allowMultiCoreMode := 0
minimizeMemUsage := 1
maxUndoLevels := 2
coreDesiredPixFmt := "0x21808" ; 24-RGB
These are meant to limit memory usage as much as possible.
Side note: the 32 bits edition is no longer actively tested.
b) How a files list index is created
=====================================================================
If a single file is loaded, QPV will set internally the variable
"mustOpenStartFolder" to the folder path where the image is from.
If the aforementioned variable is set, various user actions will
trigger the indexation of the files found in the folder using
doOpenStartFolder(). Examples of such actions: toggle thumbnails
mode, search, attempt to go to the next or previous image, and so on.
Once the folder is indexed, "mustOpenStartFolder" is set empty.
Users can open a folder using the open file/folder dialogs:
OpenDialogFiles() or OpenFolders(). Both functions, and
doOpenStartFolder(), rely internally on coreOpenFolder().
coreOpenFolder() takes as argument the folder to index files
from. If the folder string contains a pipe "|", the files will
not be indexed recursively. The function renews / clears
previous files list with renewCurrentFilesList() and then
it proceeds to scan for the files using GetFilesList().
In essence, coreOpenFolder() is a wrapper for GetFilesList(),
which uses a regular expression held in "RegExFilesPattern"
to match supported file types.
QPV internally relies on the resultedFilesList[] array object.
Each file entry is defined by various properties:
The resultedFilesList[] array object is populated by, GetFilesList(),
OpenSLD(), sldGenerateFilesList(), OpenSLDBdataBase(),
performSQLgetTable(), dbSortingCached(), SortFilesList() and others.
resultedFilesList[index_number] :=
[ 1_filePath, 2_isSelected, 3_isSeen, 4_forceThumbRefresh
, 5_isFavourite, 6_fileSize, 7_fileDateModified
, 8_fileDateCreated, 9_imgFrames, 10_mustDoSort, 11_IsImgHisto
, 12_dbRowIndex, 13_imgW, 14_imgH, 15_imgPixFmt, 16_imgWHratio
, 17_imgMGPX, 18_imgHAvg, 19_imgHmedian, 20_imgHpeak, 21_imgHlow
, 22_imgDPI, 23_dupeID, 24_imghRMS, 25_imghRange, 26_imghMode
, 27_imghMin, 28_imgHASH, 29_pixelzFsmall, 30_pixelzFbig
, 31_HpixelzFsmall, 32_HpixelzFbig, 33_HammingDist, 34_MSEscore
, 35_dateSeenDB, 36_sortModeDB]
All these properties are optional and are filled based on the
application context. 1_filePath is the only property that is not
optional, because it specifies the file path and image file at
the given index entry.
When [de]selecting a file, resultedFilesList[Index_number, 2]
is set to 1 or 0.
If "skipSeenImagesSlider" is set to 1, when an image is displayed
resultedFilesList[Index_number, 3] is set to 1. By default,
this feature is not enabled.
When GetFilesList() and coreOpenFolder() finish, in most cases,
a random image is chosen to be opened. However, when a file is
already loaded, detectFileID(imgPath) is used to identify and
set the initial file index to the image already loaded.
If a slideshow database is already opened, GetFilesList() will
automatically add the files into the database using addSQLdbEntry().
In this context, the file names are converted to lower case.
The file details related properties are optional, as previously indicated.
They are filled by GetFilesList() or collectFileInfosNow() and are used
as caches by SortFilesList(), files statistics panel and
QPV_listThumbnailsGridMode(). The collectImageInfosNow() function fills
the members of the object with details about images and their main
histogram points. These details, once cached are used for files sorting
file statistics panel and finding image duplicates.
GenerateRandyList() is called to generate a randomized array
of file index numbers from 1 to "maxFilesIndex", non-repetitive.
RandyIMGids[] array is used whenever the user navigates randomly
through the indexed files.
The current position within the index list is held by
"currentFileIndex" and the total indexed files in "maxFilesIndex".
The current «random» position is "RandyIMGnow".
Internally, "CurrentSLD" holds what folder or slideshow file
the user has opened, while "SLDtypeLoaded" indicates the type:
1 - folder
2 - slideshow plain-text
3 - SQLite slideshow database
If the user opens a slideshow file [a cached index], OpenSLD()
is invoked. This is a wrapper for sldGenerateFilesList() and
sldDataBaseOpen().
sldGenerateFilesList() parses plain-text files and it loads
INI-like saved settings using readSlideSettings().
sldDataBaseOpen() opens SQLlite3 databases and reads the
settings using IniSLDBreadAll().
c) Files index filtering
Users can apply a filter on the indexed file list through
PanelEnableFilesFilter(). It can be a RegEx or a string
that allows "|" to act as the OR operator.
coreEnableFiltru() processes the string given by the user in
"usrFilesFilteru". The internal filter is held in "filesFilter".
FilterFilesIndex() is the core function that generates the
filtered new list. A backup of the main list is created:
bckpResultedFilesList := resultedFilesList.Clone()
bkcpMaxFilesIndex := maxFilesIndex
A map of the indexes between the two arrays is created:
filteredMap2mainList[Index_number] := Index_number_main_list
filteredMap2mainList[] array is used when the user performs
actions that change the index and have to be propagated in
the main list [the backup]. For example: file rename, index
entry removal and so on.
When the new resultedFilesList[] array is created, all entry
properties are maintained.
FilterFilesIndex() can take Regular Expressions or «queries».
Strings that begin with: «SQL:query:» will be parsed and used
to perform a SELECT in the currently loaded SQL database. For
example: SQL:query:WHERE fmodified LIKE '20200607%' .
If the user does not work with an SQL database, another type
of queries can be used to filter the files list: «QPV:query:»
These are structured as follows:
QPV:query:what:minRange:maxRange:optionalString
Example: QPV:query:fsize:100002:250001:string
When such queries are performed, Quick Picto Viewer uses cached
data when available, otherwise it will extract image or file
details. The collected data is cached in memory for subsequent
queries or index sort operations. If an SQL database is opened,
the data will be stored in the database as well.
The function FilterFilesIndex() ends by invoking
GenerateRandyList().
d) How an image is loaded and displayed on screen
=====================================================================
To display an image, one can invoke IDshowImage(given_file_index)
or ShowTheImage(imgPath).
IDshowImage() retrieves the image file path based on a given
number from resultedFilesList[] array and can filter dead
files, if "skipDeadFiles" is set to 1. It can also reset
viewport viewing conditions if "resetImageViewOnChange" is
set to 1.
ShowTheImage() is called with the full file path by IDshowImage().
This function is a wrapper for coreShowTheImage() to allow the user
quickly skim through indexed files without loading and displaying
each of them. It takes into account how fast the user changes
between images and how fast they are rendered on screen. If the
system resources permit this, all images will be displayed. However,
if load times are high, then many images will be skipped. To this
end, there are various timers meant to slightly delay image loading,
to allow for interruptions.
IDshowImage(), ShowTheImage() and coreShowTheImage() are invoked
even if the user is in list mode [thumbnails view].
coreShowTheImage() ends abruptly by setting the window title
and invoking UpdateThumbsScreen() if the user is in thumbnails
mode. Otherwise, it continues by invoking ResizeImageGDIwin().
coreShowTheImage() also checks for file existence and can throw
message errors to users.
ResizeImageGDIwin() and coreShowTheImage() keep track of the
currently loaded image file and previous one, to help decide
if cached GDI bitmaps must be used or not. They can discard
all cached image data.
Images are cached as GDI+ bitmap objects by Quick Picto Viewer.
This occurs at different levels or stages. This enables users
to delete an image file, go to the next one and, if they choose,
return to the deleted one and find the cached image, and resave it.
If no cached GDI bitmaps are available or a different image
file is loaded, ResizeImageGDIwin() will call CloneMainBMP().
CloneMainBMP() manages «original» image caching and pre-processes
images for display: rotate, convert depth modes. It caches only
two images at a time. This function can abort image processing
if the user attempts to change image before it finishes. It
communicates with the interface thread by retrieving
"canCancelImageLoad". When it is set to 4, it means abort.
CloneMainBMP() is called only by ResizeImageGDIwin().
coreShowTheImage() is the only function that can invoke
ResizeImageGDIwin().
LoadBitmapForScreen() is called by CloneMainBMP(). This is
the function that accesses files. It is a wrapper for three
functions that return a GDI+ bitmap:
- LoadWICscreenImage()- loads images through Windows Imaging Component
it is the prefered loader since v5.6.0
- LoadFimFile() - used to load additional file formats
- this function converts any image to 24 or
32 bits depth and then to a GDI+ bitmap object
- LoadFileWithGDIp() - loads GDI+ supported files
- LoadFileWithWIA() - the fallback function of LoadFileWithGDIp()
for very large images
- WIA stands for Windows Image Acquisition
LoadWICscreenImage() calls DLL functions found in qpvmain.dll:
WICpreLoadImage(), WICgetRectImage() and WICdestroyPreloadedImage().
WICpreLoadImage() returns the handle and properties of the image to
be loaded. WICgetRectImage() return a GDI+ bitmap object. It is
invoked only if the image does not exceed the GDI+ limit of 536 mgpx.
When the image exceeds 536 megapixels, it is transfered to FreeImage
using teleportWICtoFIM().
LoadWICscreenImage() and LoadFimFile() can return to LoadBitmapForScreen()
the string "very-large", when an image over 536 megapixels was loaded.
In this context, the image is a FreeImage object and the "gdiBitmap"
GDI+ bitmap is just a dummy. The object is "viewportQPVimage" and it is
created via the "screenQPVimage" class.
LoadBitmapForScreen() returns a GDI+ bitmap handle on success.
When it is invoked by CloneMainBMP(), it has as argument
"allowCaching" set to 1 which allows this function to hold
in cache the previously opened file, unprocessed by CloneMainBMP().
And it also fills-in properties for currIMGdetails[], which
holds various details about the image about to be displayed.
When CloneMainBMP() finishes loading and processing the image,
in ResizeImageGDIwin() the GDI+ bitmap dimensions and user
viewing options are used to calculate the dimensions of the image
in the viewport and the selection area, if present.
QPV_ShowImgonGui() is then invoked with multiple parameters.
This is the function that will render the image on screen.
It can also generate new sets of viewport caches.
- "gdiBMPvPsize" - handle of a viewport sized image
- "gdiBitmapSmall" - handle of a low-res sized image;
- the dimensions are calculated in determineGDIsmallCacheSize()
These are obtained by resizing "gdiBitmap", the bitmap returned
by CloneMainBMP().
QPV_ShowImgonGui() reuses parts of the previously rendered image
on screen when the user pans the image, or zoom level change, but
only if "userImgQuality" is set to 1, for high-quality image
resampling. The cached bitmap is held in variable named "ViewPortBMPcache".
If the image loaded exceeds 536 megapixels, the aforementioned caches
are not created: gdiBMPvPsize, gdiBitmapSmall. QPV_ShowImgonGui() no
longer reuses parts of the previously rendered image. Instead, it
calls retrieveQPVscreenImgSection(). This function renders / displays
very large images. Internally, it caches a viewport sized image.
decideGDIPimageFX() is used to obtain GDI+ objects that can alter
the display of colors: a color matrix and an Effects object.
drawHUDelements() is the function that renders the scroll bars,
current frame index (visual indicator), the grid ( drawVPgridsNow() )
and the histogram box ( createHistogramBMP() ) on the viewport.
These UI elements are rendered on a different GDI window.
drawHUDelements() also invokes additionalHUDelements(), a function
that wraps additional HUD elements: drawImgSelectionOnWindow(),
drawAnnotationBox(), VPnavBoxWrapper(), and drawinfoBox().
additionalHUDelements() is often invoked in the code to update the
selection area and/or other HUD elements.
QPV_ShowImgonGui() tracks how fast the render on screen is and
adaptively choose between the available caches. To draw the
image on screen, Gdip_DrawImage() or Gdip_DrawImageFX() are used.
Internally, QPV use the 32-PARGB pixel format, because
Gdip_DrawImage() and other functions perform faster.
There are four EX_LAYERED windows: "hGDIthumbsWin", "hGDIwin"
"hGDIselectWin", "hGDIinfosWin". There are two global GDI+
Graphics, with their own HDCs and GDI hBitmaps: glPG / glHDC
and 2NDglPG / 2NDHDC. These are recreated upon window resize
events. These windows are created by BuildGUI() found in
"module-interface.ahk". The GDI+ canvases are created by
createGDIPcanvas() found in the main QPV.ahk file. Auxiliary
GDI+ are created in initGDIPstuff().
See section g) for more details on what windows are created.
e) General considerations
=====================================================================
When actions are performed or somehow the main core is about
to get busy, changeMcursor() and/or setImageLoading() are called.
These set variables in the user interface thread, pertaining to
the main thread. When it is busy, the interface thread changes
the mouse cursor and sets the busy task bar progress animation.
For operations that can be aborted, doStartLongOpDance()
is called.
The main thread regularly checks the other thread for specific
variables to see if the user intentions changed, to abort
the current operation[s] or continue.
One cannot have the interface thread ping the main thread
too often because it leads to «hangs». Therefore, in most
cases, I have the main thread ping the UI thread.
In most cases, if not all, I use ahkPostFunction() to call
functions from the main thread, because I want to avoid locking
the UI.
For more details about the interface, please see section g).
Many GDI+ functions are wrapped to keep track of the bitmaps
created and to avoid memory leaks. These are prefixed with
"trGdip_*".
f) How slideshows happen
=====================================================================
When the user starts a slideshow, dummyInfoToggleSlideShowu()
is called. It gives the user information about the slideshow.
This is a wrapper for ToggleSlideShowu() which is responsible
for toggling the slideshow.
The whole slideshow «business» is convoluted. The purpose
was to obtain an easy to stop slideshow in any circumstance,
even if it is running very fast or the user loads very large
images and applies all the possible real-time effects.
To this end, it relies on the interfaceThread[] object,
however not entirely. The main thread checks if the UI thread
is still game for slideshows, and if it is, it invokes a timer
in the UI thread: invokeExternalSlideshowHandler().
This happens after every image. In the UI thread, the script
can decide the slideshow must end [based on user activity]
and the timer never executes. When another image is loaded,
the main thread learns it ended.
On start, ToggleSlideShowu() calls slideshowsHandler() found
in the interfaceThread[] object. For each slide, through
invokeExternalSlideshowHandler() the function
dummySlideshow() in the UI thread is called and a timer is set
for theSlideShowCore(). This last function, when invoked, calls
in the main thread one of these functions: RandomPicture(),
PreviousPicture() or NextPicture() - based on user preference.
[ side note: notice the ping-pongs between the threads ^_^ ]
"SlideHowMode" is the variable desginating the slideshow mode
and "slideShowDelay" is the delay used for the aforementioned
timer, in miliseconds. "slideShowRunning" is a boolean. These
variables and additional ones are used in both threads and
often synchronized...
Things get even uglier with GIF animations...
autoChangeDesiredFrame() is used for animated GIFs and it is
run with the first parameter set to "start" only from
QPV_ShowImgonGuiPrev(). And it is also invoked as a timer.
Because I wanted users to have the option to allow GIFs to
play entirely before going to the next slide, the main thread
sets "allowNextSlide" to 0 while the GIF plays and when the
first loop is done, it is set to 1 and theSlideShowCore()
function is called from the UI thread, and
invokeExternalSlideshowHandler() to set there the timer
for the next slide.
g) Windows, AHK GUIs, Menus, Toolbar
=====================================================================
The application consists mainly of two threads. In the main
thread, all the processing happens. The other thread is
dedicated to the user interface: "module-interface.ahk".
In the main file, the interface thread is simply referenced
as an object named "interfaceThread". This separation was made
to provide users an optimal experience when using QPV, to
avoid unnecessary stalling or freezes of the application.
The following is a list, a summary, of what functions, found
in the main thread, draw what and on which window(s).
All the windows are owned by the "PVhwnd" window.
These are created in the interface thread, from BuildGUI().
0. PVhwnd - main window frame of QPV
various text controls are added
1. hGDIthumbsWin - glPG - layered window
QPV_listThumbnailsGridMode() - thumbnails grid for the list mode
QPV_ShowThumbnails() - draws and generates thumbnails
During live image editing
QPV_ShowImgonGui() - image on screen
drawHUDelements() - histogram box [image view only]
various UI markers [frames counter, scrollbars]
2. hGDIwin - glPG - layered window
livePreviewsImageEditing() - the live image editing tools
corePasteInPlaceActNow()
coreDrawLinesSelectionArea()
coreFillSelectedArea()
livePreviewInsertTextinArea()
livePreviewEraseArea()
livePreviewDesaturateArea()
... and others
When NOT in live image editing
QPV_ShowImgonGui() - image on screen
drawVPgridsNow() - viewport grid
drawHUDelements() - histogram box [image view only]
various UI markers [frames counter, scrollbars]
3. hGDIselectWin - 2NDglPG - layered window
mainGdipWinThumbsGrid() - thumbnails grid overlay, scrollbar and status bar
additionalHUDelements() - wrapper for the following functions
drawImgSelectionOnWindow() - selection area [image view only]
drawinfoBox() - infos box
VPnavBoxWrapper() - image navigation/preview box
drawAnnotationBox() - image captions box [image view only]
drawLiveCreateShape() - live shape drawing [image view only]
4. hGDIinfosWin - 2NDglPG - layered window
CreateOSDinfoLine() - user OSD messages
MouseMoveResponder() - hover selection area handler
5. ShowClickHalo() - on-demand, temporary window, invoked
only by the main thread.
6. mouseCreateOSDinfoLine() - mouseToolTipGuia; on-demand,
temporary window. It displays messages for the user, where
the mouse is located.
If the aforementioned windows, from 0 to 4, fail to be created,
Quick Picto Viewer will throw a warning and offers to exit or
continue as options.
The main thread creates on user demand the following AHK windows:
1. PanelFoldersTree() - folders tree navigation and manager
hfdTreeWinGui, fdTreeGuia
2. PanelQuickSearchMenuOptions() - searchable menu options
hquickMenuSearchWin, QuickMenuSearchGUIA
It can also function as a folders manager and navigation tool,
if a folder path is provided, instead of keywords.
3. createSettingsGUI() - generalized panel, it is used by most
of the panels users can see in QPV. Each panel has an ID,
assigned to the "AnyWindowOpen" variable.
hSetWinGui, SettingsGUIA
It must be noted that some panels are constructed by
fakeWinCreator() which uses msgBoxWrapper() to create
simple panels.
repositionWindowCenter() is called after any panel is created.
This function repositions the window in the center of the
screen where the main QPV is found. It also applies the dark
mode theme on all its controls.
4. msgBoxWrapper() - custom message boxes of all kinds
MsgBox2hwnd, WinMsgBox
5. createGUItoolbar() - customizable toolbar
hQPVtoolbar, OSDguiToolbar
6. CreateSoloSliderWidgetWin() - a sliders widget
hSliderWidget, SoloSliderWidgetGUIA
It is used as a popup for some tools found in the
toolbar.
7. CreateCollapsedPanelWidget() - collapsed panel widget
collapseWidgetGUIA, collapseWidgetGUIA
It is invokable by collapsing the panel of a live tool.
The main thread shows messages in the viewport using
CreateOSDinfoLine() wrapped by showTOOLtip().
The main GUI of the QPV window, created in BuildGUI(),
contains multiple static text controls. These are used
as place holders. They are updated with texts pertaining
to the various viewport elements. The functions that
update the controls are uiAccess*(). They are invoked from
the main thread by drawHUDelements() or other associated
functions. This is meant to help make the viewport more
accessible for users relying on screen readers.
\\Contexts:
Toolbar and menu related functions decide what options to display
based on the app/user context and user settings. Broadly speaking
there are several modes and their associated global variables:
- thumbnails list view
var: thumbsDisplaying = 1
- image view
var: thumbsDisplaying = 0
- live image editing tools
var: imgEditPanelOpened = 1, AnyWindowOpen > 0
- alpha masking brush mode (sub-mode of live image editing, never otherwise)
Detectable via: isNowAlphaPainting()
- freeform vector shapes editor (sub-mode of image view, never otherwise)
var: drawingShapeNow = 1
These contexts apply to the keyboard shortcuts as well.
\\The custom controls:
In the panels created with createSettingsGUI(), many controls,
even if not actually customized, are wrapped just for the
sake of improving accessibility for screen readers and to add
tooltips easily, eg. GuiAddEdit().
The custom sliders are created with GuiAddSlider() and respond to
user input via GuiSlidersResponder(). When created, they are
usually set to invoke the panel's specific UI function prefixed
with "updateUI*".
\\The menus:
The main thread creates all the context menus via
InitGuiContextMenu(), InitSecondMenu() and others. The menus are
displayed with showThisMenu() from the main thread, The menu bar
itself is created in the interface thread, but it is a dummy, fake.
The menu bar items invoke functions from the main thread that display
the sub-menu itself. ShowThisMenu() identifies the position of the
menu bar clicked item and positions the menu to be displayed. The
menu bar functions in the main file begin with "InvokeMenuBar*".
The menu bar in the interface thread is generated with BuildMenuBar().
Why did I do it like this? I found no way to dynamically attach from
the main thread the menus to the interface thread window. I hate
when apps display all kinds of pointless options that are not
relevant to the current context, e.g., Text menu in Affinity Photo
when no text object is manipulated. It is all a clutter. As such,
I find it is imperative to generate the list of menus and sub-menus
dynamically, to only display what is relevant.
All the menus, and the context menus, are created with the standard
Menu command of AHK, however I created a wrapper function for it.
The list of menus is generated on the go, on demand, based on
app/user context[s]. The wrapper helps keep track of the menus
generated: kMenu(). It records menu names, states and keywords.
objuA - menu items by index with properties objuA[menuIndex] := [prop1, prop2,... etc]
objuB - maps menu-names with menu items pairs to indexexes of objuA; objuB[menu "-" label] := menuIndex
objuC - menu-names and their properties objuC[menu] := [menu-name, container-menu, accelerator-key]
The menus are searchable via PanelQuickSearchMenuOptions() -->
PopulateQuickMenuSearch() --> buildQuickSearchMenus(). The global
variable "mustPreventMenus" is set to 1 before generating the
searchable list of the menus. When this is used, some menu items
are hidden, while others are added, and the menus themselves are
not invoked. When this variable is set to 1, showThisMenu() does
not display the menu on screen.
\\The toolbar:
The GUI of the toolbar is created by CoreGUItoolbar().
createGUItoolbar() is a wrapper that minimizes that filters the
calls to the main function and loads the toolbar settings.
CoreGUItoolbar() holds the list of possible toolbar icons and
their properties in the form of:
localStaticBtnID := [ico-file-name, 0, base-user-friendly-name, 0, button-ID, 0, menu-mode, modes-availability]
Buttons in the toolbar are added via tlbrAddNewIcon(). The list
of those added is held in this object: "tlbrIconzList".
tlbrIconzList[btnID/hwnd/index] := [ctrl-hwnd, icoFile, btnName, btnType, indexBtn, function-to-call, icoW, icoH, param-func, btnID, menuMode]
The buttons are typical AHK / Win32 button controls customized
using the ImageButton class. It is a class that assigns ImageLists
to push buttons using the ImageList_Create Win32 API function.
The toolbar can be customized by users via PanelCustomizeToolbar()
The custom toolbar is loaded only if userCustomizedToolbar equals 1.
"userThumbsToolbarList" and "userImgViewToolbarList" hold the lists
of user customized toolbar for thumbnails list and image view.
The other modes cannot be customized.
WM_MOUSEMOVE() from the main thread invokes tlbrDecideTooltips()
to dynamically display tooltips for the toolbar buttons, which
can change based on the context.
The toolbar buttons actions are invoked via tlbrInvokeFunction() -
a wrapper that utilized "tlbrIconzList" to decide what function
has to be called. The decision is made primarily in
processToolbarFunctions().
The buttons can have left and right click actions associated.
Some of them invoke CreateSoloSliderWidgetWin(), eg.,
tlbrFloodFillSlidersInvoker(). These are pop-up sliders. Others,
allow users to click and drag to adjust a value, eg.,
tlbrChangeStuffRotation().
h) Input responses
=====================================================================
All keyboard shortcuts are «registered» through WM_KEYDOWN and
WM_SYSKEYDOWN. Both, the main thread and the interface one, register
these Window Messages. The function invoked when the messages are
issued is, in both threads, WM_KEYDOWN(). The function is meant to
finish execution as soon as possible. constructKbdKey() is called to\
convert the virtual keys into readable form. In KeyboardResponder(),
main thread, is where each keyboard shortcut is actually registered,
where the code decides what functions to call based on the keyboard
input, and the user context, in-app conditions. QPV allows custom
defined keyboard shortcuts. These are stored as an object:
"userCustomKeysDefined". The object is initialized by
loadCustomUserKbds(). In KeyboardResponder(), if the user-pressed
key is not customized [or defined] in the aforementioned object,
the key is passed to processDefaultKbdCombos().
Users can define custom keyboard shortcuts through
PanelQuickSearchMenuOptions(), by right-clicking on any item found
in the search results list. PanelDefineKbdShortcut() is the panel
that allows users to define the new keyboard shortcut,
via BtnApplyNewKbdShortcut().
Mouse input is mostly handled in the interface thread.
The Window Messages for mouse buttons (e.g., WM_LBUTTONUP)
are registered and, broadly speaking, invoke WinClickAction(),
which is a wrapper to the function with the same name from
the main thread. The function handles most of the imagined
responses to mouse clicks, based on user context. Several
examples:
- thumbsListClickResponder()
- ActPaintBrushNow()
- ActDrawAlphaMaskBrushNow()
- addNewVectorShapePoints()
- updateTinyPreviewArea()
... and many more: WinClickAction() has over 700 lines...
WM_MOUSEMOVE message is registered in the interface thread
and it is associated with the function WM_MOUSEMOVE().
This function changes mouse cursor to different states,
based on the user context and calls MouseMoveResponder()
from the main thread. which handles live viewport updates
related to hover actions.
WM_MOUSEMOVE message is registered in the main thread
as well, associated with WM_MOUSEMOVE(). Most importantly,
it displays tooltips when users hover icons in the QPV toolbar,
if it is visible. The toolbar is created by createGUItoolbar(),
if "ShowAdvToolbar" equals 1.
i) Image live editing
=====================================================================
The tool panels are created using createSettingsGUI() with the
parameter "isImgLiveEditor" set to 1. If the panel does indeed open,
then "imgEditPanelOpened" global variable is set to 1. These can be
invoked by keyboard shortcuts, menus or via the toolbar.
When a live editor panel is opened, it can be collapsed into a small
widget. The widget is created with CreateCollapsedPanelWidget(). The
viewport is also responsive to clicks and keyboard actions specific
to this mode. The toolbar is also updated with specific icons, based
on the active tool, via createGUItoolbar() and CoreGUItoolbar().
Such panels have their controls attached via g-labels to a
function prefixed with "updateUI*", eg. updateUIfillPanel(). Such
functions update the status of the different available controls: hides,
shows, disables, or enables controls.
The Apply button in these panels is usually attached to
applyIMGeditFunction(). The function is just a wrapper which invokes
a specific function for each tool, eg. BtnDrawShapeSelectedArea(),
based on the "AnyWindowOpen" value. Most of these functions are named
with this pattern: "Btn*SelectedArea". These functions invoke the
actual functions that output the final result, on the image itself.
The live preview in the viewport is handled by
corelivePreviewsImageEditing(). This is another wrapper that handles
how the live preview is drawn on screen and other things. It invokes
specific function for each tool, eg. livePreviewInsertTextinArea().
based on the "AnyWindowOpen" value. Most live preview functions
that render the preview on screen have this prefix: "livePreview*".
Dedicated efforts have been put into generating live previews that do
not exceed the viewport area, in order to maintain the performance
within acceptable levels, while maintaining fidelity as much as possible
with the final output when the apply button is pushed by users. Many
functions cache generated bitmaps to avoid re-rendering everything,
e.g., generateAlphaMaskBitma() will reuse the cached bitmap when the
preview relies on the same alpha mask options.
When viewport color adjustments are activated
mergeViewPortEffectsImgEditing() is called first, before the actual
function of the tool that applies the modifications to the main bitmap.
Flood fill and the brush tool, even if labeled as live tools, they do
not have live preview functions. The actions are live themselves.
The functions are: ActFloodFillNow() and ActPaintBrushNow().
Some tools allow users to paint in the alpha mask, eg., fill selected
area. When the user activates this option, toggleAlphaPaintingMode()
is called. The live brush response during painting is handled by
ActDrawAlphaMaskBrushNow() together with the specific live preview
function of the active tool. In this mode, one undo level is recorded.
When drawing tools are used: "liveDrawingBrushTool" is set to 1.
The aforementioned functions, ActDrawAlphaMaskBrushNow(), ActFloodFillNow()
and ActPaintBrushNow() are invoked by WinClickAction() when users click
on the canvas.
When images are edited, two images are continually updated:
"UserMemBMP" and gdiBitmap. "gdiBitmap" is used for the viewport.
"UserMemBMP" is the edited bitmap.
When editing images under 536 megapixels, 100 levels are recorded,
via wrapRecordUndoLevelNow(). Otherwise, one undo level is recorded
by recordUndoLevelHugeImagesNow(). The undo/redo actions are handled
by ImgUndoAction() and ImgRedoAction(). The array "undoLevelsArray"
holds the IDs of the different undo states and GDI+ bitmaps.
When editing images above 536 megapixels, the image data is handled
through FreeImage and most of the tools have counterparts that work
on the FreeImage bitmap. "UserMemBMP" and "gdiBitmap" are not used.
The array "undoLevelsArray" is also not used with such large bitmaps.
The image data is accessible via "viewportQPVimage.imgHandle"
property and the methods of "screenQPVimage" class. The image editing
functions that rely on FreeImage, for very large images, begin with
the prefix "HugeImages*".
For images under 536 megapixels, the entire image is recorded for
every undo level and is always on 32-bits RGBA. But for bigger images,
only a subsection of the image is recorded, when possible, and it can
be at a color depth of 24-bits or 32-bits.
Many of the image editing tools rely on C++ functions invoked via
DLL calls from "qpvmain.dll" written by me.
Garbage collection is mainly performed via resetMainWin2Welcome().
In this function, various objects are discarded, including viewport
image caches and image undo levels.
j) How to enable alpha masking for an image editing live tool
=====================================================================
1. Add the window ID to isAlphaMaskWindow().
Window ID is "AnyWindowOpen". The panels are created using
createSettingsGUI().
2. Add uiADDalphaMaskTabs(tab-ID-A, tab-ID-B, funcUIpanel) in the
function creating the panel of the tool, eg.,
PanelFillSelectedArea(). This will add the specific alpha masking
options/controls in the panel.
tab-ID-A and B are the tab order in the panel created.
"funcUIpanel" is the reference to the function of the panel
used to update the user interface elements and the viewport,
eg., updateUIfillPanel().
3. Add in UItriggerBrushUpdate() a pointer to "funcUIpanel"(),
as explained previously, based on the "AnyWindowOpen" value.
Do the same in BtnToggleBrushColors().
4. Add in "funcUIpanel"() calls to updateUIalphaMaskStuff(1) and
updateUIalphaMaskStuff(2) based on the "CurrentPanelTab". This
will make the alpha mask controls' options to show or hide
as expected by users.
5. Define the "viewportDynamicOBJcoords" object properties: x, y,
w, h and zl. It must be defined inside the live preview drawing
function of the tool itself, eg., livePreviewAdjustColorsArea().
The object is used by MouseMoveResponder().
6. The live preview drawing function must generate a bitmap and
then pass it to realtimePasteInPlaceAlphaMasker() based on
decideAlphaMaskingFeaseable(). See livePreviewAdjustColorsArea()
as an example. Alternatively, when alpha mask painting mode is
activated, for performance considerations, a direct call to
QPV_SetAlphaChannel() is suggested, as in
livePreviewAdjustColorsArea().
7. Update toggleAlphaPaintingMode(), the commented section «tools
with no object as reference», and «create new bitmap for these
tools». Add the AnyWindowOpen ID of the tool to the isVarEqualTo()
if condition. The user alpha mask bitmap must be created based on
the selection area dimensions.
k) How vector shapes drawing works
=====================================================================
When users start drawing a vector shape, startDrawingShape() is
invoked. The function prepares a bunch of things.
The global variable named "drawingShapeNow" is set to 1. If the
selection area has a vector shape already defined,
resumeCustomShapeSelection() is invoked to convert the points
data to the edit mode data.
At every mouse move drawLiveCreateCustomShape() is invoked by
additionalHUDelements() to refresh the window. The function
caches the points data. When the data is cached. it is rendered
by drawVisibleVectorPoints() invoked from drawLiveCreateCustomShape().
When the user clicks in the viewport, WinClickAction() invokes
PerformVectorShapeActions(). This function is also called by
some context menu options listed by
createContextMenuCustomShapeDrawing().
When the user exits the tool, stopDrawingShape() is called.
This function converts and saves the points data for later use.
Users can load and save vector shapes through PanelManageVectorShapes().
Undo levels in the vector editor mode are handled by
recordVectorUndoLevels() and restoreGivenVectorUndoLevel().
\\Technicalities:
The points in edit mode are stored in an array object:
customShapePoints[index] := [x, y]
X and Y values are floating points based on the dimensions of the
image painted in the viewport, at current zoom level. The values
are not capped in the range of 0 to 1.
The top/left corner is defined by "prevDestPosX" and "prevDestPosY".
These two variables designate where in the viewport was last painted
the image according to the current zoom level and panning.
Please see getVectorCoordsFromVPpoint() and getVPcoordsVectorPoint()
to find out the exact formulas.
Outside edit mode:
The vector points are stored as floating points from 0 to 1 in the same
array object: customShapePoints. The values are used to calculate the
coordinates of the points for GDI+, based on the width and height of
the viewport selectionn area bounding box. When one draws on screen with
GDI+, one must use the viewport coordinates to match with client window.
One of the relevant functions is convertCustomShape2givenArea() called
by createImgSelPath().
In the same format, the points are stored when a user chooses to save
to disk a vector shape via saveCurrentVectorShape().
l) How image duplicates identification works (soon)
=====================================================================
PanelFindDupes()
retrieveDupesByProperties()
OpenSLDBdataBase()
collectImageInfosNow()
collectSQLFileInfosNow()
generateSQLimageFingerPrintHash()
filterDupeResultsByHdist()
corefilterDupeResultsByHdist()
changeHdistLevelCached()
sortDupeGroups()
PanelAutoSelectDupes()
autoSelectDupesInGroups()
m) How to add a new panel/tool (soon)
=====================================================================