-
Notifications
You must be signed in to change notification settings - Fork 0
/
Main.hs
1542 lines (1380 loc) · 59.1 KB
/
Main.hs
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
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE RecursiveDo #-}
module Main where
-- base
import Control.Monad
( unless, void, when )
import Data.Foldable
( for_, traverse_ )
import Data.List
( elemIndex )
import Data.Maybe
( catMaybes, fromMaybe, fromJust, isNothing )
import Data.Traversable
( for )
import Data.Word
( Word32, Word64 )
import GHC.Stack
( HasCallStack )
import System.Environment
( setEnv )
import System.Exit
( ExitCode(..), exitWith, exitSuccess )
-- containers
import Data.Map.Strict
( Map )
import qualified Data.Map.Strict as Map
import Data.Set
( Set )
import qualified Data.Set as Set
import Data.Sequence
( Seq )
import qualified Data.Sequence as Seq
-- directory
import qualified System.Directory as Directory
( canonicalizePath )
-- haskell-gi-base
import qualified Data.GI.Base as GI
import qualified Data.GI.Base.GObject as GI
import qualified Data.GI.Base.GType as GI
import qualified Data.GI.Base.GValue as GI
import qualified Data.GI.Base.Overloading as GI
-- gi-gdk
import qualified GI.Gdk as GDK
-- gi-gio
import qualified GI.Gio as GIO
-- gi-gobject
import qualified GI.GObject as GObject
-- gi-gtk
import qualified GI.Gtk as GTK
-- stm
import qualified Control.Concurrent.STM.TVar as STM
import qualified Control.Concurrent.STM.TMVar as STM
import qualified Control.Monad.STM as STM
-- text
import Data.Text
( Text )
import qualified Data.Text as Text
-- gtk-layers
import qualified Paths_gtk_layers as Cabal
( getDataFileName )
--------------------------------------------------------------------------------
-- Main application --
----------------------
main :: IO ()
main = do
-- Some useful environment variables:
--
setEnv "GDK_SCALE" "2" -- scale the UI up by a factor
--setEnv "GSK_RENDERER" "ngl" -- choose rendering backend
--setEnv "GTK_DEBUG" "actions,layout" -- GTK debug info
--setEnv "GDK_DEBUG" "events,opengl,dnd" -- GDK debug info
--setEnv "GSK_DEBUG" "full-redraw" -- force redraws
application <- GTK.applicationNew ( Just "com.layers" ) [ ]
GIO.applicationRegister application ( Nothing @GIO.Cancellable )
void $ GIO.onApplicationActivate application ( runApplication application )
exitCode <- GIO.applicationRun application Nothing
GObject.objectUnref application
case exitCode of
0 -> exitSuccess
_ -> exitWith ( ExitFailure $ fromIntegral exitCode )
-- | UI elements that might need to be dynamically edited.
data UIElements
= UIElements
{ layersContainer :: !GTK.ScrolledWindow
-- ^ Container for the layer hierarchy display.
, layersListModel :: !GTK.TreeListModel
-- ^ The underlying 'GTK.TreeListModel' of the layer hierarchy.
, layersDebugLabel :: !GTK.Label
-- ^ Label that displays the layer hierarchy textually.
, undoButton, redoButton :: !GTK.Button
}
-- | Global state of the application.
data Variables
= Variables
{ uniqueTVar :: !( STM.TVar Unique )
-- ^ 'STM.TVar' used as an unique supply.
, historyTVar :: !( STM.TVar History )
-- ^ The application state, enabling undo/redo.
-- | This TVar allows us to look up which 'GIO.ListStore' is used
-- for the children of a given parent.
--
-- This allows us to know, given a parent and a child index,
-- how to insert/delete from the 'GTK.TreeListModel'.
, parStoreFromUniqTVar :: !( STM.TVar ( Map ( Parent Unique ) GIO.ListStore ) )
-- | This TMVar is used to ensure that the layer hierarchy data
-- is kept in-sync between the application and the UI's 'GTK.TreeListModel'.
, listModelUpToDateTMVar :: !( STM.TMVar () )
}
runApplication :: GTK.Application -> IO ()
runApplication application = do
window <- GTK.applicationWindowNew application
GTK.setWindowTitle window "Layers"
GTK.windowSetDefaultSize window 640 480
display <- GTK.rootGetDisplay window
cssProvider <- GTK.cssProviderNew
themePath <- Directory.canonicalizePath =<< Cabal.getDataFileName "theme.css"
GTK.cssProviderLoadFromPath cssProvider themePath
GTK.styleContextAddProviderForDisplay display cssProvider 1000
GTK.widgetAddCssClass window "gtk-layers"
uniqueTVar <- STM.newTVarIO @Unique testInitialUnique
historyTVar <- STM.newTVarIO @History testInitialHistory
parStoreFromUniqTVar <- STM.newTVarIO Map.empty
listModelUpToDateTMVar <- STM.newTMVarIO ()
let variables = Variables { uniqueTVar, historyTVar, parStoreFromUniqTVar, listModelUpToDateTMVar }
contentPane <- GTK.panedNew GTK.OrientationHorizontal
-- Set the paned handle to "wide", otherwise the handle can eat input around it.
GTK.panedSetWideHandle contentPane True
layersScrolledWindow <- GTK.scrolledWindowNew
GTK.scrolledWindowSetPolicy layersScrolledWindow GTK.PolicyTypeNever GTK.PolicyTypeAutomatic
leftBox <- GTK.boxNew GTK.OrientationVertical 10
buttonsBox <- GTK.boxNew GTK.OrientationHorizontal 10
GTK.widgetSetValign buttonsBox GTK.AlignEnd
GTK.widgetSetHalign buttonsBox GTK.AlignCenter
GTK.widgetSetMarginBottom leftBox 10
layersDebugLabel <- GTK.labelNew ( Just $ uncurry prettyLayers $ present testInitialHistory )
GTK.widgetSetVexpand layersDebugLabel True
GTK.boxAppend leftBox layersDebugLabel
GTK.boxAppend leftBox buttonsBox
GTK.panedSetStartChild contentPane ( Just leftBox )
GTK.panedSetEndChild contentPane ( Just layersScrolledWindow )
newLayerButton <- GTK.buttonNewWithLabel "New layer"
newGroupButton <- GTK.buttonNewWithLabel "New group"
deleteLayerButton <- GTK.buttonNewWithLabel "Delete"
undoButton <- GTK.buttonNewWithLabel "Undo"
redoButton <- GTK.buttonNewWithLabel "Redo"
traverse_ ( GTK.boxAppend buttonsBox )
[ newLayerButton, newGroupButton, deleteLayerButton, undoButton, redoButton ]
-- Undo/redo buttons should only be active when there is something to undo/redo.
traverse_ ( \ button -> GTK.widgetSetSensitive button False )
[ undoButton, redoButton ]
layersListModel <- newLayersListModel variables
let uiElts =
UIElements
{ layersContainer = layersScrolledWindow
, layersListModel
, layersDebugLabel
, undoButton, redoButton
}
layersView <- newLayerView uiElts variables
GTK.listViewSetShowSeparators layersView False
GTK.scrolledWindowSetChild layersScrolledWindow ( Just layersView )
void $ GTK.onButtonClicked newLayerButton $ do
u <- getUnique uniqueTVar
mbSelectedItem <- getSelectedItem layersView
let change = NewItem { newUnique = u, newIsGroup = False, newSelected = mbSelectedItem }
updateLayerHierarchy uiElts variables ( DoChange change )
void $ GTK.onButtonClicked newGroupButton $ do
u <- getUnique uniqueTVar
mbSelectedItem <- getSelectedItem layersView
let change = NewItem { newUnique = u, newIsGroup = True, newSelected = mbSelectedItem }
updateLayerHierarchy uiElts variables ( DoChange change )
void $ GTK.onButtonClicked deleteLayerButton $ do
mbSelectedItem <- getSelectedItem layersView
case mbSelectedItem of
Nothing ->
-- The SingleSelection SelectionModel defaults to autoselect=true,
-- so there should always be an item selected.
return ()
Just selItem -> do
let change = Delete { deleteItem = selItem }
updateLayerHierarchy uiElts variables ( DoChange change )
void $ GTK.onButtonClicked undoButton $ do
updateLayerHierarchy uiElts variables UndoChange
void $ GTK.onButtonClicked redoButton $ do
updateLayerHierarchy uiElts variables RedoChange
GTK.windowSetChild window ( Just contentPane )
GTK.widgetSetVisible layersView True
GTK.widgetSetVisible window True
void $ GTK.onApplicationQueryEnd application $
GTK.applicationRemoveWindow application window
--------------------------------------------------------------------------------
-- Application and UI state --
------------------------------
--------------
-- App data --
--------------
-- | A unique identifier; can be used for any kind of object.
newtype Unique = Unique { unique :: Word64 }
deriving newtype ( Eq, Ord, Enum )
instance Show Unique where { show ( Unique i ) = "%" ++ show i }
-- | Get the next 'Unique', incrementing the counter by one.
getUnique :: STM.TVar Unique -> IO Unique
getUnique uniqueTVar =
STM.atomically $
STM.stateTVar uniqueTVar ( \ old -> ( old, succ old ) )
-- | The layer hierarchy.
--
-- This representation allows easy modifications, e.g. to move an item around
-- one simply needs to edit the map in two places, removing the item from its
-- old parent and adding it to its new parent.
--
-- This representation is chosen over a recursive data structure such as:
--
-- > type LayerHierarchy = [ Layer ]
-- > data Layer = Layer { .. } | Group { .., groupChildren :: LayerHierarchy }
--
-- as such a representation is more difficult to edit (in particular when
-- deeply nested items are involved).
type LayerHierarchy = Map ( Parent Unique ) ( Maybe [ Unique ] )
-- TODO: use IntMap
-- | The application state, with history for undo/redo.
data History
= History
{ past :: !( Seq ( Content, Diff ) )
, present :: !( Content, Metadata )
, future :: ![ ( Diff, Content ) ]
}
-- | Main application content, subject to undo/redo.
data Content =
Content
{ layerHierarchy :: !LayerHierarchy
-- layerData :: !( Map Unique SomeData )
}
deriving stock Show
-- | Auxilary metadata about the application, not subject to undo/redo.
data Metadata =
Metadata
{ names :: !( Map Unique Text )
, invisibles :: !( Set Unique )
}
-- | A parent of an item.
data Parent a
-- | The item is at the top level.
= Root
-- | The item has this parent.
| Parent !a
deriving stock ( Show, Eq, Ord, Functor )
-----------------
-- GTK UI data --
-----------------
-- | Look up the name and visibility of a layer from the metadata.
layerNameAndVisible :: Metadata -> Unique -> ( Text, Bool )
layerNameAndVisible ( Metadata { names, invisibles } ) unique =
( names Map.! unique, unique `notElem` invisibles )
-- | Display the layer hierarchy (for debugging purposes).
prettyLayers :: Content -> Metadata -> Text
prettyLayers ( Content { layerHierarchy } ) meta =
Text.intercalate "\n" $
concatMap go ( fromMaybe [] $ layerHierarchy Map.! Root )
where
go :: Unique -> [ Text ]
go i =
let ( name, visible ) = layerNameAndVisible meta i
in
case layerHierarchy Map.! Parent i of
Nothing ->
[ "- Layer { layerName = \"" <> name <> "\", layerVisible = " <> Text.pack ( show visible ) <> " } " ]
Just cs ->
[ "- Group { layerName = \"" <> name <> "\", layerVisible = " <> Text.pack ( show visible ) <> " }" ]
++ concatMap ( map ( " " <> ) . go ) cs
-- | Custom GTK object used to hold layer data.
--
-- These are the items that will get stored in the ListModel used by GTK
-- to store the layer hierarchy data.
newtype LayerItem = LayerItem ( GTK.ManagedPtr LayerItem )
instance GI.TypedObject LayerItem where
glibType = GI.registerGType LayerItem
instance GI.GObject LayerItem
instance GI.HasParentTypes LayerItem
type instance GI.ParentTypes LayerItem = '[ GObject.Object ]
-- | Data associated to a 'LayerItem', for use in the GTK UI state.
data LayerID
= GroupID { layerUnique :: !Unique }
| LayerID { layerUnique :: !Unique }
deriving stock ( Eq, Show )
instance GI.DerivedGObject LayerItem where
type GObjectParentType LayerItem = GObject.Object
type GObjectPrivateData LayerItem = Maybe LayerID
objectTypeName = "gtk-layers-LayerItem"
objectClassInit _ = return ()
objectInstanceInit _ _ = return Nothing
objectInterfaces = [ ]
--------------------------------------------------------------------------------
-- GTK TreeListModel --
-----------------------
-- | Create a new 'GTK.TreeListModel' from the given set of layers.
newLayersListModel :: Variables -> IO GTK.TreeListModel
newLayersListModel ( Variables { .. } ) = do
itemType <- GI.glibType @LayerItem
store <- GIO.listStoreNew itemType
STM.atomically $
STM.modifyTVar' parStoreFromUniqTVar ( Map.insert Root store )
( Content { layerHierarchy }, _ ) <- present <$> STM.readTVarIO historyTVar
let topLayers = fromMaybe [] $ layerHierarchy Map.! Root
for_ topLayers $ \ layerUniq -> do
let mbChildren = layerHierarchy Map.! Parent layerUniq
layer = case mbChildren of
Nothing -> LayerID { layerUnique = layerUniq }
Just {} -> GroupID { layerUnique = layerUniq }
item <- GI.unsafeCastTo LayerItem =<< GI.new LayerItem []
GI.gobjectSetPrivateData item ( Just layer )
GIO.listStoreAppend store item
rootModel <- GIO.toListModel store
let passthrough = False -- Must not use passthrough to use TreeExpander widgets.
autoExpand = True -- Autoexpand on creation; we later set this to False.
-- Pass a copy of the (reference to the) root GIO.ListStore to the
-- 'treeListModelNew' function to ensure we retain ownership of it.
model <- GI.withManagedPtr rootModel $ \ rmPtr ->
GI.withNewObject rmPtr $ \ rmCopy ->
GTK.treeListModelNew rmCopy passthrough autoExpand createChildModel
-- After the initial model has been created, we set "autoexpand" to False,
-- so that if we move a non-expanded group, it doesn't get automatically
-- expanded after it is moved.
GTK.treeListModelSetAutoexpand model False
return model
where
createChildModel :: GObject.Object -> IO ( Maybe GIO.ListModel )
createChildModel parent = do
dat <- getLayerData =<< GTK.unsafeCastTo LayerItem parent
case dat of
LayerID {} -> return Nothing
GroupID { layerUnique = groupUnique } -> do
( Content { layerHierarchy }, _ ) <- present <$> STM.readTVarIO historyTVar
let children = fromMaybe [] $ layerHierarchy Map.! Parent groupUnique
-- Try to re-use an existing list store, if there is one.
mbOldChildStore <-
STM.atomically $ do
mbOldStore <-
Map.lookup ( Parent groupUnique ) <$> STM.readTVar parStoreFromUniqTVar
when ( isNothing mbOldStore ) $
-- Take a lock to avoid creating multiple child stores
-- for the same group.
STM.takeTMVar listModelUpToDateTMVar
return mbOldStore
newChildStore <-
case mbOldChildStore of
Just oldStore -> do
return oldStore
Nothing -> do
-- Otherwise, create a new child ListModel.
-- NB: create a simple GIO.ListStore, not a nested GTK.TreeListModel,
-- as that would cause e.g. grand-children models to be created twice.
itemType <- GI.glibType @LayerItem
childStore <- GIO.listStoreNew itemType
for_ children $ \ childUniq -> do
let mbChildChildren = layerHierarchy Map.! Parent childUniq
childLayer = case mbChildChildren of
Nothing -> LayerID { layerUnique = childUniq }
Just {} -> GroupID { layerUnique = childUniq }
item <- GI.unsafeCastTo LayerItem =<< GI.new LayerItem []
GI.gobjectSetPrivateData item ( Just childLayer )
GIO.listStoreAppend childStore item
-- Store the child store in our mapping from parent unique to
-- ListStore, so that we know where to insert children.
STM.atomically $ do
STM.modifyTVar parStoreFromUniqTVar
( Map.insert ( Parent groupUnique ) childStore )
STM.putTMVar listModelUpToDateTMVar ()
return childStore
-- Pass a copy of the (reference to the) child store
-- to ensure we retain ownership of it.
childModelCopy <- GI.withManagedPtr newChildStore $ \ childStorePtr ->
GI.withNewObject childStorePtr $ \ childStoreCopy ->
GIO.toListModel childStoreCopy
return $ Just childModelCopy
-- | Gets the 'LayerItem' for a row in a 'GTK.TreeListModel'
-- with @passthrough = False@.
treeListItemLayerItem :: GTK.ListItem -> IO LayerItem
treeListItemLayerItem listItem = do
mbListRow <- GTK.listItemGetItem listItem
case mbListRow of
Nothing -> error "treeListItemLayerItem: ListItem has no item"
Just listRow ->
treeListRowLayerItem =<< GTK.unsafeCastTo GTK.TreeListRow listRow
-- | Retrieve the 'LayerItem' underlying a 'GTK.TreeListRow'.
treeListRowLayerItem :: GTK.TreeListRow -> IO LayerItem
treeListRowLayerItem listRow = do
mbListRowItem <- GTK.treeListRowGetItem listRow
case mbListRowItem of
Nothing -> error "treeListRowLayerItem: TreeListRow has no item"
Just item -> GTK.unsafeCastTo LayerItem item
-- NB: if you made the mistake of passing a 'createChildModel' function
-- which recursively creates a TreeListModel, you would have to recurse
-- into the row using 'treeListRowGetItem' to eventually get at the
-- underlying data.
-- | Class for objects which wrap a 'LayerItem'.
class HasLayerData a where
-- | Get the layer data associated to a 'LayerItem' or object
-- containing a 'LayerItem'.
getLayerData :: HasCallStack => a -> IO LayerID
instance HasLayerData LayerItem where
getLayerData item = do
mbDat <- GI.gobjectGetPrivateData item
case mbDat of
Nothing -> error "getLayerData: no private data"
Just dat -> return dat
instance HasLayerData GTK.TreeListRow where
getLayerData row = do
parLayerItem <- treeListRowLayerItem row
getLayerData parLayerItem
instance HasLayerData GTK.ListItem where
getLayerData listItem = do
layerItem <- treeListItemLayerItem listItem
getLayerData layerItem
-----------------------
-- GTK layer widgets --
-----------------------
-- | The generic widget used to display a list item.
--
-- Structure:
--
-- - ListItem
-- - TreeExpander
-- - ContentBox
-- - CheckButton
-- - Label
data LayerViewWidget =
LayerViewWidget
{ layerViewContentBox :: GTK.Box
, layerViewCheckButton :: GTK.CheckButton
, layerViewLabel :: GTK.EditableLabel
}
newLayerViewWidget :: IO GTK.TreeExpander
newLayerViewWidget = do
expander <- GTK.treeExpanderNew
GTK.treeExpanderSetIndentForIcon expander True
GTK.treeExpanderSetIndentForDepth expander True
GTK.treeExpanderSetHideExpander expander False
contentBox <- GTK.boxNew GTK.OrientationHorizontal 20
GTK.treeExpanderSetChild expander ( Just contentBox )
checkBox <- GTK.checkButtonNew
GTK.boxAppend contentBox checkBox
itemLabel <- editableLabelNew
GTK.boxAppend contentBox itemLabel
return expander
-- | Create a new editable label, but remove any 'DragSource' or 'DropTarget'
-- controllers attached to it, as we don't want the label to participate in
-- drag-and-drop operations, especially because having it participate in
-- drag-and-drop operations triggers segfaults due to a GTK bug.
editableLabelNew :: IO GTK.EditableLabel
editableLabelNew = do
label <- GTK.editableLabelNew " "
widget <- GTK.toWidget label
removeControllers widget
return label
where
removeControllers widget = do
controllers <- GTK.widgetObserveControllers widget
nbControllers <- GIO.listModelGetNItems controllers
unless ( nbControllers == 0 ) $
for_ [ 0 .. nbControllers - 1 ] $ \ i -> do
mbController <- GIO.listModelGetItem controllers i
for_ mbController $ \ controller -> do
mbDrag <- GTK.castTo GTK.DragSource controller
mbDrop <- GTK.castTo GTK.DropTarget controller
for_ mbDrag $ GTK.widgetRemoveController widget
for_ mbDrop $ GTK.widgetRemoveController widget
mbChild <- GTK.widgetGetFirstChild widget
case mbChild of
Nothing -> return ()
Just c -> do
removeControllers c
removeControllersSiblings c
removeControllersSiblings c = do
mbNext <- GTK.widgetGetNextSibling c
case mbNext of
Nothing -> return ()
Just next -> do
removeControllers next
removeControllersSiblings next
-- | Get the widget hierarchy for a list item, so that we can modify
-- the wdigets to display the appropriate content.
getLayerViewWidget :: GTK.TreeExpander -> IO LayerViewWidget
getLayerViewWidget expander = do
mbContentBox <- GTK.treeExpanderGetChild expander
case mbContentBox of
Nothing -> error "getLayerViewWidget: expected ListItem->Expander->Box"
Just contentBox0 -> do
contentBox <- GTK.unsafeCastTo GTK.Box contentBox0
mbCheckButton <- traverse ( GTK.unsafeCastTo GTK.CheckButton ) =<< GTK.widgetGetFirstChild contentBox
case mbCheckButton of
Nothing -> error "getLayerViewWidget: expected ListItem->Expander->Box->CheckButton"
Just checkButton -> do
mbLayerLabel <- traverse ( GTK.unsafeCastTo GTK.EditableLabel ) =<< GTK.widgetGetNextSibling checkButton
case mbLayerLabel of
Nothing -> error "getLayerViewWidget: expected ListItem->Expander->Box->{CheckButton,LayerLabel}"
Just layerLabel ->
return $
LayerViewWidget
{ layerViewContentBox = contentBox
, layerViewCheckButton = checkButton
, layerViewLabel = layerLabel
}
------------------
-- GTK ListView --
------------------
-- | Create a new 'GTK.ListView' that displays 'LayerItem's.
newLayerView :: UIElements -> Variables -> IO GTK.ListView
newLayerView
uiElts@( UIElements { layersListModel, layersContainer, layersDebugLabel } )
variables@( Variables { .. } ) = mdo
layersListFactory <- GTK.signalListItemFactoryNew
-- Connect to "setup" signal to create generic widgets for viewing the tree.
--
-- We attach a collection of signals to each widget,
-- to handle e.g. drag-and-drop operations.
--
-- We attach the signals in the "setup" phase, because we don't want
-- to have to keep attaching/removing event controllers to the widgets
-- (in the "bind" and "unbind" stages).
--
-- However, in the "setup" phase, we don't yet know which underlying ListModel
-- item we are displaying (this is only set on "bind").
-- So: how can we set signal handlers in the "setup" phase? The answer is that
-- each signal handler will read the private data associated to the widget;
-- this data gets set when binding the widget to its ListModel item.
_ <- GTK.onSignalListItemFactorySetup layersListFactory $ \ listItem0 -> do
listItem <- GTK.unsafeCastTo GTK.ListItem listItem0
GTK.listItemSetFocusable listItem False
expander <- newLayerViewWidget
GTK.listItemSetChild listItem ( Just expander )
GTK.widgetAddCssClass expander "layer-item"
LayerViewWidget
{ layerViewLabel = label
, layerViewCheckButton = visibleButton }
<- getLayerViewWidget expander
----------------------------
-- Visibility CheckButton --
----------------------------
void $ GTK.onCheckButtonToggled visibleButton $ do
uniq <- layerUnique <$> getLayerData listItem
visible <- GTK.checkButtonGetActive ?self
( newContent, newMetadata ) <- STM.atomically $ do
hist@History { present = ( content, meta@( Metadata { invisibles } ) ) } <- STM.readTVar historyTVar
let invisibles'
| visible
= Set.delete uniq invisibles
| otherwise
= Set.insert uniq invisibles
meta' = meta { invisibles = invisibles' }
hist' = hist { present = ( content, meta' ) }
STM.writeTVar historyTVar hist'
return ( content, meta' )
let newDebugText = prettyLayers newContent newMetadata
GTK.labelSetText layersDebugLabel newDebugText
-------------------
-- EditableLabel --
-------------------
-- Connect a signal for editing the layer name.
--
-- NB: we don't use the 'onEditableChanged' signal, as that updates
-- after every key stroke.
void $ GI.after label ( GI.PropertyNotify #hasFocus ) $ \ _ -> do
newText <- GTK.editableGetText label
dat <- getLayerData listItem
History { present = ( newContent, newMetadata ) } <- STM.atomically $ do
hist@History { present = ( layers, meta@( Metadata { names } ) ) } <- STM.readTVar historyTVar
let names' = case dat of { LayerID { layerUnique = u } -> Map.insert u newText names
; GroupID { layerUnique = u } -> Map.insert u newText names }
meta' = meta { names = names' }
hist' = hist { present = ( layers, meta' ) }
STM.writeTVar historyTVar hist'
return hist'
let newDebugText = prettyLayers newContent newMetadata
GTK.labelSetText layersDebugLabel newDebugText
----------------
-- DragSource --
----------------
-- Connect signals for starting a drag from this widget.
dragSource <- GTK.dragSourceNew
GTK.dragSourceSetActions dragSource [ GDK.DragActionCopy ]
void $ GTK.onDragSourcePrepare dragSource $ \ _x _y -> do
srcUniq <- layerUnique <$> getLayerData listItem
mbTreeListRow <- traverse ( GTK.unsafeCastTo GTK.TreeListRow ) =<< GTK.listItemGetItem listItem
treeListRow <- case mbTreeListRow of
Nothing -> error "newLayerView ListItem onSetup: no TreeListRow"
Just r -> return r
srcPar <- getParent treeListRow
rowPos <- GTK.treeListRowGetPosition treeListRow
GTK.singleSelectionSetSelected selectionModel rowPos
let dnd_sourceItem =
WithinParent
{ parent = fmap snd srcPar
, item = srcUniq
}
val <- GDK.contentProviderNewForValue =<< GIO.toGValue ( GI.HValue dnd_sourceItem )
GTK.widgetAddCssClass layersContainer "dragging-item"
return $ Just val
void $ GTK.onDragSourceDragBegin dragSource $ \ _drag -> do
{- To set a cursor icon for the drag, write the x/y coordinates in the
'prepare' signal handler to an IORef, and then use the following:
( x, y ) <- readIORef dragPosRef
paintable <- GTK.widgetPaintableNew ( Just expander )
GTK.dragSourceSetIcon ?self ( Just paintable ) ( round x ) ( round y )
-}
noPaintable <- GDK.paintableNewEmpty 0 0
GTK.dragSourceSetIcon ?self ( Just noPaintable ) 0 0
GTK.widgetAddCssClass expander "dragged"
-- TODO: add "dragged" class for all descendants as well.
void $ GTK.onDragSourceDragCancel dragSource $ \ _drag _reason -> do
GTK.widgetRemoveCssClass layersContainer "dragging-item"
GTK.widgetRemoveCssClass expander "dragged"
return True
-- ^^^^ Important. Setting this to 'False' stops GDK
-- from properly clearing the drag cursor.
void $ GTK.onDragSourceDragEnd dragSource $ \ _drag _deleteData -> do
GTK.widgetRemoveCssClass layersContainer "dragging-item"
GTK.widgetRemoveCssClass expander "dragged"
----------------
-- DropTarget --
----------------
-- Connect signals for receiving a drop on this widget.
dropTarget <- GTK.dropTargetNew GI.gtypeHValue [ GDK.DragActionCopy ]
let dropTargetCleanup = do
GTK.widgetRemoveCssClass expander "drag-over"
GTK.widgetRemoveCssClass expander "drag-top"
GTK.widgetRemoveCssClass expander "drag-bot"
mbNextItem <- getNextItem_maybe expander
for_ mbNextItem $ \ nextItem -> do
GTK.widgetRemoveCssClass nextItem "drag-top"
void $ GTK.onDropTargetAccept dropTarget $ \ _drop -> do
return True
--dat <- getLayerData listItem
--case dat of
-- GroupID {} -> return True
-- LayerID {} -> return True
void $ GTK.onDropTargetDrop dropTarget $ \ val _x y -> do
dropTargetCleanup
dropTgtUniq <- layerUnique <$> getLayerData listItem
GI.HValue dragSrc@( WithinParent { item = dragSrcUniq }) <-
GIO.fromGValue @( GI.HValue UniqueWithParent ) val
mbTreeListRow <- traverse ( GTK.unsafeCastTo GTK.TreeListRow ) =<< GTK.listItemGetItem listItem
treeListRow <- case mbTreeListRow of
Nothing -> error "newLayerView ListItem onSetup: no TreeListRow"
Just r -> return r
dstFlatIndex <- GTK.treeListRowGetPosition treeListRow
h <- GTK.widgetGetHeight expander
let droppedAbove = y < 0.5 * fromIntegral h
expanded <- GTK.treeListRowGetExpanded treeListRow
dstPar <- getParent treeListRow
isDescendant <- isDescendantOf dragSrcUniq listItem
let mbDropIntoGroup
| expanded
, not droppedAbove
, not isDescendant
= Just treeListRow
| otherwise
= Nothing
mbDropOutsideGroup
| dragSrcUniq == dropTgtUniq
, Parent par <- dstPar
, not droppedAbove
= Just par
| otherwise
= Nothing
if isDescendant && isNothing mbDropOutsideGroup
then do
return False
else do
-- Compute the destination parent.
-- Usually, the destination parent is the parent of the drop target.
-- BUT:
-- 1. when dropping an item into the first position of an
-- expanded group, the destination parent is the drop target itself,
-- not the parent of the drop target.
-- 2. when an item is at the bottom of a group, dropping it on its
-- lower half moves the item out of the group, so the
-- destination parent is the grand-parent of the drop target.
( dropDst, newPosInTree ) <-
if
-- (1)
| Just dstParRow <- mbDropIntoGroup
-> do
dstParFlatIndex <- GTK.treeListRowGetPosition dstParRow
return $
( MoveToTopOfGroup dropTgtUniq
, dstParFlatIndex + 1
)
-- (2)
| Just ( dstParRow, dstParUniq ) <- mbDropOutsideGroup
-> do
grandPar <- getParent dstParRow
return $
( MoveItemOutsideGroupIfLastItemInGroup
{ itemUnique = dropTgtUniq
, parentUnique = dstParUniq
, grandParentUnique = fmap snd grandPar
, itemExpanded = expanded
}
, dstFlatIndex + 1
)
| otherwise
-> do
return $
( MoveAboveOrBelow
{ moveDstItem =
WithinParent
{ parent = fmap snd dstPar
, item = dropTgtUniq
}
, moveAbove = droppedAbove
}
, if droppedAbove then dstFlatIndex else dstFlatIndex + 1
)
-- Compute the position that the item we are moving will have
-- at the end of the move.
--
-- First, we compute whether we moved up or down.
-- NB: we need to compute the source item position now (using 'treeListRowGetPosition'),
-- at the end of the drag-and-drop operation, because TreeExpander nodes
-- might have expanded/collapsed in the meantime.
mbSelItem <- GTK.singleSelectionGetSelectedItem selectionModel
mbSelIx <- for mbSelItem $ \ selItem -> do
selRow <- GTK.unsafeCastTo GTK.TreeListRow selItem
GTK.treeListRowGetPosition selRow
-- Now compute the final destination position.
mbDstPosAfterShift <-
case mbSelIx of
Nothing ->
return Nothing
Just selIx
-- If we moved up, simply use the destination position.
| selIx >= newPosInTree
-> return $ Just newPosInTree
| otherwise
-> do
-- If we moved down, we need to substract the number of items
-- moved. Note that this depends on which TreeExpander nodes
-- are expanded.
mbSelRow <- GTK.treeListModelGetRow layersListModel selIx
case mbSelRow of
Nothing -> return Nothing
Just selRow0 -> do
selRow <- GTK.unsafeCastTo GTK.TreeListRow selRow0
nbDescendants <- getNbExpandedDescendants layersListModel selRow
return $
if newPosInTree < nbDescendants
then Nothing
else Just $ newPosInTree - nbDescendants
updateLayerHierarchy uiElts variables $
DoChange $
Move
{ moveSrc = dragSrc
, moveDst = dropDst
}
-- After moving, update the selected item to be the moved item.
case mbDstPosAfterShift of
Nothing -> return ()
Just dstPos ->
GTK.singleSelectionSetSelected selectionModel dstPos
return True
void $ GTK.onDropTargetEnter dropTarget $ \ _x y -> do
GTK.widgetAddCssClass expander "drag-over"
h <- GTK.widgetGetHeight expander
if y < 0.5 * fromIntegral h
then do
GTK.widgetAddCssClass expander "drag-top"
else do
GTK.widgetAddCssClass expander "drag-bot"
mbNextItem <- getNextItem_maybe expander
for_ mbNextItem $ \ nextItem -> do
GTK.widgetAddCssClass nextItem "drag-top"
return [ GDK.DragActionCopy ]
void $ GTK.onDropTargetMotion dropTarget $ \ _x y -> do
h <- GTK.widgetGetHeight expander
if y < 0.5 * fromIntegral h
then do
GTK.widgetRemoveCssClass expander "drag-bot"
GTK.widgetAddCssClass expander "drag-top"
mbNextItem <- getNextItem_maybe expander
for_ mbNextItem $ \ nextItem -> do
GTK.widgetRemoveCssClass nextItem "drag-top"
else do
GTK.widgetRemoveCssClass expander "drag-top"
GTK.widgetAddCssClass expander "drag-bot"
mbNextItem <- getNextItem_maybe expander
for_ mbNextItem $ \ nextItem -> do
GTK.widgetAddCssClass nextItem "drag-top"
return [ GDK.DragActionCopy ]
void $ GTK.onDropTargetLeave dropTarget $ do
dropTargetCleanup
GTK.widgetAddController expander dragSource
GTK.widgetAddController expander dropTarget
-- Connect to "bind" signal to modify the generic widget to display the data for this list item.
_ <- GTK.onSignalListItemFactoryBind layersListFactory $ \ listItem0 -> do
listItem <- GTK.unsafeCastTo GTK.ListItem listItem0
mbExpander <- GTK.listItemGetChild listItem
expander <-
case mbExpander of
Nothing -> error "layerView onBind: list item has no child"
Just expander0 -> GTK.unsafeCastTo GTK.TreeExpander expander0
LayerViewWidget
{ layerViewCheckButton = checkButton
, layerViewLabel = layerLabel
} <- getLayerViewWidget expander
dat <- getLayerData listItem
( _content, meta ) <- present <$> STM.readTVarIO historyTVar
-- All we do is set the name and visibility of this layer/group.
let ( layerText, checkBoxStatusVisible ) =
case dat of
LayerID { layerUnique } ->
layerNameAndVisible meta layerUnique
GroupID { layerUnique } ->
layerNameAndVisible meta layerUnique
mbTreeListRow <- traverse ( GTK.unsafeCastTo GTK.TreeListRow ) =<< GTK.listItemGetItem listItem
treeListRow <- case mbTreeListRow of
Nothing -> error "newLayerView ListItem onBind: no TreeListRow"
Just r -> return r
GTK.treeExpanderSetListRow expander ( Just treeListRow )
case dat of
LayerID {} -> do
GTK.widgetSetVisible checkButton True
GTK.checkButtonSetActive checkButton checkBoxStatusVisible
GroupID {} -> do
GTK.widgetSetVisible checkButton True
GTK.checkButtonSetActive checkButton checkBoxStatusVisible
GTK.editableSetText layerLabel layerText
-- Pass copies of (references to) the TreeListModel and SelectionModel,
-- in order to retain ownership over them.
selectionModel <- GI.withManagedPtr layersListModel $ \ lmPtr ->
GI.withNewObject lmPtr $ \ lmCopy ->
GTK.singleSelectionNew ( Just lmCopy )
listView <- GI.withManagedPtr selectionModel $ \ smPtr ->
GI.withNewObject smPtr $ \ smCopy ->
GTK.listViewNew ( Just smCopy ) ( Just layersListFactory )
return listView
-- | Get the next item in the flattened tree, if any.
-- Ignores any notion of parent/child.
--
-- This is used to style the "item below" the current drop target,
-- to account for the fact that the item below is rendered on top of the item
-- above it, which overdraws any shadow/glow extending downwards on the latter.
getNextItem_maybe :: GTK.TreeExpander -> IO ( Maybe ( GTK.TreeExpander ) )
getNextItem_maybe expander = do
mbParent <- GTK.widgetGetParent expander
case mbParent of
Nothing -> error "nextItem: item has no parent"
Just parent -> do
mbNextItemParent <- GTK.widgetGetNextSibling parent
case mbNextItemParent of
Nothing -> return Nothing
Just nextItemParent -> do
mbNextItem <- GTK.widgetGetFirstChild nextItemParent
case mbNextItem of
Nothing -> error "nextItem: next item has no child"
Just nextItem0 -> do
nextItem <- GTK.unsafeCastTo GTK.TreeExpander nextItem0
return $ Just nextItem
-- | Is this list item a descendant of the item with the given unique?
isDescendantOf :: Unique -- ^ are we a descendant of this?
-> GTK.ListItem -- ^ item we are querying
-> IO Bool
isDescendantOf u listItem = do
mbListRow <- GTK.listItemGetItem listItem
case mbListRow of
Nothing -> error "isDescendantOf: ListItem has no item"
Just listRow0 -> do
listRow <- GTK.unsafeCastTo GTK.TreeListRow listRow0
go listRow