%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /proc/self/root/lib/gnome-shell/
Upload File :
Create Path :
Current File : //proc/self/root/lib/gnome-shell/libshell-14.so

ELF>@��,@8@�c�cppp
�
�000`}(`}(@�,@�,@�,�,�20�,0�,0�,�����  ���$$S�td���  P�td!,!,!,Q�tdR�td@�,@�,@�,�+�+GNU�GNU�{1�xi*A�,��w��z��Od )��h�a��P�##�*�B@�Rq��J@@���TZL�"�G:� �� dDAH�B
��Md <4T �Xij B@�S,a���+���B
�e�#�AB�� 
@�Hg	@�U�Y`k`� 0!"(,���$�6�Ea�#P5���`20�j�@ $R3T1 �5�T�_9�PCmj(@V�n)��defghilnoprtvwxyz|}~�������������������������������������������������������������������������������������������������������	
 !#$%&'(*+,./02345678:;<=>?@BCEFGHIJKMNPQSTUVXYZ\]^`bcdefgijklmnopqrstuvxyz|}~�����������������������������'Z���\z+o��yO�qe��|>齾�Ut+&7��x�8R�Ɛ���,�̢���D�|_�?�(�Jz��<������ S��k�
V���G��$Q�"���ȵ�P���(�q��١ϵک�l�)?�_)f]&�C��*@��>!%�n?6|j:ע	�f�QH��9��rq˲q��s!�Ltj��"e��3�g�VBy�N>�gC�Ϧ��^[9����9��0
�c�cE����XC��c�֥q��
�X��}b����]�ߐ�_u�T�}x���QA9;���!��C�O��t��&��W� =��ל����/g�vuJ���4���{/���+q����Ǥ~	���
��<�w\�_	0j��Q��L(��=�J��(��Ǣ��۝ϣ��a�n�N���m��x�߂�q�}��.;�r�U���gM�=D�����T�9'�nn����Gٵ��]�_:��<zSo�/�϶��v�k.�!ʋ9��{FՌ$�,6YB
��)��v�_���|M�I���ejI�%��ߝ/ӌ�˛pE=tf�����g���p��[��Y�w���`�Z����jsg�������;�T'����C��K��9BmS;�E��Z�s�	�ni��ą�07�aMC>�kJ�5��A�SQ�kJ�G�D�7[H'���a^��kJ�����}�i?�l�W��H�~�w�c��6A]]�Ӛ�ܝ�;��t��˘
�.G�zTs��v��4������x�ܠ�a���#Y�9O�4����B���9ݪ@��I7�iv�>��RC���\
#���UW�)��F@�]-�,�IN�U@
�vK�?B6A#�xl��­�d&�ß�(=%7�t[\�9�ͦ4c�~E�)(����"y�"�w�ͅqy7.��op������$���	Sp��"��9a8%��LW�j-S�����!�q@���'~)�!X����3�t��u7��i͕��f��Ix�MR����)�&8�77)^�u�.����-:$���Jis����C
�m��Նh��i��~�O�>�B�ϩK��몹�}��n�胠{	1'a�j_%ð/h�V�����k�w�8�2�O���eQ���,�_Q����6�?�0��X���,�JUdG|�7�;�Ռ���>�҄4H'��)�uv�c&�k���ku�@�3rԨ[�����.�k�B��W�����i�U�'��n-%TFwD���iQ(ؒ/�^(�=�C����x�=*��Ԁ�`s,�U_�g�YQ:Z�n�8��G�*�Au`N@Z�fv�5�h*	p0(g]31-�M�E�b�]�bB$;�n�`��,Gb�h_�9���$J�)�Z��1�88�1�r`qVKiZ}7\T��1�Z��u�-�/)>-VG:{r��g�g��c�j�9�?Aq�\�fVU-h�H=�qufCc�e��.v�d>�p^�7<O"�W8M1�#B�CcCH>z.�[�A�:��1�l:_A�Te;\

�M$B�hET�Ak+b�U���N�sj]o�:WAF"[?�>\-bX�;B]Gs:��n�4�f�6�7Fd���<�=�e�`�vV�8�;&c(v�Z�+Fv_6&�D�e,#�_�1q�J/r)(5Bk
tr�^Dt�	��_
b��8��b�%5<`'8�&�O��[N�`U#�+��4�
�3t�%&�.y	�D�f:+_a<�H!Ph h�3�sA/�LUA(>d��i�
_t�a�U	�K�\+
�],%�F�&�r1
�;FBf
6�+�<�/.E��.|a�KPbg�B�@�o!
�CMCRj�>�Z�:�4�G�0J52�CO.l8�;� �-�8Hj`�
�
�39AuE�[h�:OsyhN�fYiovsq����O�2<v�ngd,�9>7�)�=F%��&r-�iE�gho�,gG�:a\
�?�MZsHt�1�%�B�$�bm�ba�G
db;��:#)w"�^$4��a�Cr<�FkBs�k�t^7�/!4#]U�=]d�C=`+�Zc�"ud	�E;:�8^�^Du�,_�\�5nj�Y�ug�rY8�O�7<k4� K8�gX@��\�G�j$yGaS�@�3�^	;e��M.~��h55�.��av�#��0+��A�0q*t2�
�,=1�
D&�KO9�P#UC�3�Q��6,AdY�e�<�?�_�d���*t?S�A�c�-b��1�B�+�*St�.@#�o(6P0jbC��@eB"gV/�*l7�5f(,MY�W�	�\"����u9#H>?�"X�pQW�>��	� �d
c$?#u��5I<��Bgh973Y#�b?\;5[�c�i�D�[�c�Q��$�=.=�B�7C"D}E�f	�r=t~p{o;@f{�C	fum	z>"�5"5�4Z�'ff��*�C�
�(��j-s�9�<�6-9�h�>y;�@�T+tO2�Q�7"<%tD	-\-:�4�9�;�$+$��/^��<�7�'�h�M�rX.�>��uJ+�Cs$�&/F�T?�s\�gz
�_p@K
�"�	7uc��<�eJtGI5�5aV7m;�Dis�>IHH6�p�@�?�f�*Pf�an>���_O�8l3��7�	
C�0>;S�6��[�
�).Q
�n��
�G}\nA�p�h�ZS�sC.���B�Hr�"3�os6�=�s�U�c&9E 90�?�6��#6o.:o_�adh?Ia%�5�[:2~[�?>["�7,L�=��:X`�
?Cm+�J0'PX�s-kU>�0
O��P6�e�h�	\D'/"�a�b�`��m[(�Z�b# c2xs~g�?�$, �;�`�F8�@�a� 6D��;*d-X2�*qe�;/@�^�2NE�	�f��dW,:�pM�-W;+���i0�
�V`P	E�E0�H�gPDJ�G ��&@l�N0&a9�IpJ%68#gt��:p��m�_pr%� JX�T�K�&J���3��U�N��Y�YbZm�V�l�[0l��m�)�#�e�$)�sgpP��s�H�k���l`�+\L`<Q 4.Y Y�}' nGP`C���7)(�pp�!0\m�p��m(kн=L�mH4���]�i�N[`^B0�f)�t`�V�P	p�taKp�m���j��
�K�Y�I`�t�lP�H�T�Jm*&�h|�UpONI�Jpm�PTi��m4_�ph&�i��3��k�*=n0���!�\m�L�k(0qm;j`�
;�CW2��m��_�o

V�O-i=��+u!�[m��H��( s�gp �5�PHF�)�wlP�/y�R�c��& �I�NP$�zK�Y�R�>}VPUSp�0�Y@ZY�/��X�X0XmvL���9��T�Jm�0F�n^�k"�n����m@��`'`m��)wm!K��j��u�Em6r `�IsR=��r�)i[�^B�p`� �q���VF�O�.W `XFNOp-m�R�?rTJmrQ�4��k0�I�Y�Z�Lr0
#8���}�G��q@
��S�CY�L��m��0`n����)@u��Pp0YFW@Q(pAY~F��U�M��%@hFq��(�r�VpPU5i �
�UGf5����#�e�7�_r`
A%0g#q�cV�O	E-��M@6Am��y B�`F�	Q�2G�v"�m��m�3�Q#I`�F@
	�M�!S�' p�E)0t�NP"i��FFzX�T:�&�j3I'Pm	�M�!T�S�5q��T`K[W`Qd+*0y!�CA�(�rdXpUm&�h#/G`�q �(�qy�F���tp&Y&pi0=N�#1�U�MBQl��I�IP�l��m�J03_N�#3�0��W�PO��@��k����BW�I�E G��k�mI 
Y�X/�"�` P�AA`k���U�}�6��pT�+U�L�e#peP�.^b�S�*�x?�KP��)pwm`E.L��p
	-m@�H��V��20��+����S C�^Pn1PR�;�2P��TM	�E0
	�i@�
J$@f�ol�I�0E.nmp��2[^B�E 
	$O�,��3@�'�P0m�N$D�QP7Rp9DFV�O,�W�Qm�I0�"`d�T0E5^Pk-�m��4R�:,N�"�CKm^S�B�Oo �mn�9M��Dk��Y@[��W@Rmljp�
�X`W�=F�	�-ЇN
0ImJ�mS�@���D��'@nm! `W��0����k���S�C
k��XT�Im�PA�H�-`���q���L��lp��kU�M1��i��X�XB"oP��W^�k0/p�+PU�q@Z�( rm�iP�
.�4�:![��N�(�l0��G�}-�S�'�o�M�m�o�Y W0Q	XQ@4mU0L�5��M0 MZ1�S�W�R�]`j��l����!\q@mM*`y$IJ�0��yO�-�#ef�S@CmIGp& 4�__gmon_start___ITM_deregisterTMCloneTable_ITM_registerTMCloneTable__cxa_finalizeg_freeg_object_unrefg_value_unsetg_strv_lengthg_strcmp0g_assertion_message_exprg_strv_get_typeg_type_nameg_logg_value_get_ucharg_value_get_booleang_value_get_intg_value_get_uintg_value_get_int64g_value_get_uint64g_value_get_doubleg_value_get_stringg_value_get_variantg_variant_equalg_value_get_boxedg_value_peek_pointerg_value_set_booleang_return_if_fail_warningg_intern_static_stringg_type_register_static_simpleg_dbus_method_invocation_get_typeg_signal_accumulator_true_handledg_signal_newg_param_spec_booleang_object_interface_install_propertyg_type_class_peek_parentg_type_class_adjust_private_offsetg_datalist_clearg_dbus_proxy_get_cached_propertyg_dbus_gvariant_to_gvalueg_variant_unrefg_variant_type_checked_g_dbus_gvalue_to_gvariantg_variant_newg_dbus_proxy_callg_value_set_variantg_object_notifyg_variant_getg_dbus_interface_info_lookup_propertyg_quark_try_stringg_datalist_id_set_data_fullg_variant_iter_nextg_variant_iter_free__stack_chk_failg_type_add_instance_privateg_dbus_proxy_call_finishg_quark_to_stringg_error_freeg_variant_get_booleang_object_class_find_propertyg_value_initg_object_get_propertyg_dbus_error_quarkg_set_errorg_object_set_propertyg_variant_builder_initg_dbus_interface_skeleton_get_object_pathg_dbus_interface_skeleton_get_connectiong_variant_take_refg_variant_builder_addg_variant_builder_endg_mutex_lockg_mutex_unlockg_list_free_fullg_source_destroyg_main_context_unrefg_mutex_clearg_value_copyg_object_notify_by_pspecg_idle_source_newg_source_set_priorityg_object_refg_source_set_callbackg_source_set_nameg_source_attachg_source_unrefg_mutex_initg_main_context_ref_thread_defaultg_malloc0g_variant_ref_sinkg_dbus_interface_skeleton_get_connectionsg_dbus_connection_emit_signalg_variant_builder_clearg_list_prependg_object_freeze_notifyg_object_thaw_notifyg_param_spec_uintg_param_spec_variantg_variant_get_uint32meta_plugin_get_typeg_object_class_install_propertiesmeta_window_get_workspaceg_slist_prependmeta_window_showing_on_its_workspacemeta_window_get_user_timeshell_app_compareg_signal_emitg_spawn_close_pidg_child_watch_addg_dbus_connection_call_finishg_task_return_errorg_task_return_booleang_bus_get_finishg_task_get_source_objectg_app_info_get_idg_strdupstrrchrstrcmpg_strconcatg_task_get_cancellableg_task_get_task_datag_dbus_connection_callg_source_removeg_hash_table_destroyst_texture_cache_get_defaultst_texture_cache_rescan_icon_themeg_strdup_printfg_data_output_stream_put_stringg_markup_escape_textg_hash_table_insertg_ascii_strtodg_markup_error_quarkg_ascii_strtoullg_file_readg_markup_parse_context_newg_markup_parse_context_parseg_input_stream_readg_markup_parse_context_freeg_input_stream_closeg_get_real_timeg_hash_table_iter_initg_hash_table_iter_nextg_hash_table_iter_removeg_value_set_intg_value_set_enumg_value_set_floatclutter_actor_meta_get_actorclutter_effect_get_typecogl_pipeline_copyclutter_get_default_backendclutter_backend_get_cogl_contextcogl_pipeline_newcogl_pipeline_set_layer_null_texturecogl_pipeline_set_layer_filterscogl_pipeline_set_layer_wrap_modecogl_color_init_from_4fcogl_pipeline_set_colorcogl_pipeline_set_uniform_1fcogl_texture_2d_new_with_sizecogl_pipeline_set_layer_texturecogl_offscreen_new_with_texturegraphene_matrix_init_translategraphene_matrix_scalecogl_framebuffer_set_projection_matrixclutter_paint_context_get_stage_viewclutter_actor_get_transformed_positionclutter_actor_get_transformed_sizeclutter_stage_view_get_scaleclutter_stage_view_get_layoutclutter_actor_box_set_originclutter_actor_box_set_sizeclutter_actor_box_scaleclutter_actor_get_paint_opacityclutter_actor_get_allocation_boxclutter_actor_box_clamp_to_pixelclutter_actor_box_get_sizeclutter_actor_node_newclutter_paint_node_add_childclutter_paint_node_unrefclutter_actor_get_sizeclutter_pipeline_node_newclutter_paint_node_set_static_nameclutter_paint_node_add_rectangleclutter_layer_node_new_to_framebufferclutter_blur_node_newcogl_texture_get_widthcogl_texture_get_heightclutter_actor_box_get_originclutter_paint_context_get_framebufferclutter_blit_node_newclutter_blit_node_add_blit_rectanglegraphene_matrix_init_scaleclutter_transform_node_newcogl_pipeline_get_uniform_locationcogl_snippet_newcogl_pipeline_add_snippetpw_proxy_get_user_datag_timeout_add_oncepw_proxy_add_listenerpw_proxy_add_object_listenerg_ptr_array_addpw_proxy_get_bound_idg_ptr_array_removepw_context_connectg_ptr_array_set_sizepw_proxy_destroypw_core_disconnectg_ptr_array_unrefpw_context_destroypw_deinitpw_loop_destroyg_ptr_array_new_fullpw_initpw_loop_newg_source_newg_source_add_unix_fdpw_context_newg_cancellable_cancelg_hash_table_unrefg_getenvg_build_filenameg_file_testg_get_user_data_dirg_mkdir_with_parentsg_file_new_for_pathXDisplayNameg_get_user_runtime_dirg_settings_newg_strsplitgjs_context_get_typeg_object_newg_strfreevg_file_equalg_file_hashg_hash_table_new_fullg_cancellable_newg_bus_watch_nameg_mallocg_dbus_proxy_set_cached_propertyg_io_error_quarkg_error_matchesg_clear_errorshell_action_mode_get_typeg_once_init_enterg_flags_register_staticg_once_init_leaveshell_app_state_get_typeg_enum_register_staticshell_app_launch_gpu_get_typeshell_blur_mode_get_typeg_param_spec_intg_param_spec_floatg_param_spec_enumshell_snippet_hook_get_typeshell_network_agent_response_get_typeg_static_resource_initg_static_resource_finishell_org_gtk_application_interface_infog_dbus_proxy_set_interface_infoshell_org_gtk_application_override_propertiesg_object_class_override_propertyshell_org_gtk_application_get_typeg_once_init_enter_pointerg_type_interface_add_prerequisiteg_once_init_leave_pointerg_dbus_interface_info_lookup_signalg_variant_n_childreng_malloc0_ng_value_set_objectg_variant_iter_initg_variant_iter_next_valueg_signal_lookupg_signal_emitvg_dbus_proxy_get_typeg_type_add_interface_staticg_dbus_method_invocation_get_method_infog_unix_fd_list_get_typeg_dbus_method_invocation_get_messageg_dbus_message_get_unix_fd_listg_dbus_method_invocation_return_errorg_dbus_interface_skeleton_get_typeshell_org_gtk_application_get_busyg_type_check_instance_is_ag_type_interface_peekshell_org_gtk_application_set_busyg_object_setshell_org_gtk_application_call_activateshell_org_gtk_application_call_activate_finishshell_org_gtk_application_call_activate_syncg_dbus_proxy_call_syncshell_org_gtk_application_call_openshell_org_gtk_application_call_open_finishshell_org_gtk_application_call_open_syncshell_org_gtk_application_call_command_lineshell_org_gtk_application_call_command_line_finishshell_org_gtk_application_call_command_line_syncshell_org_gtk_application_complete_activateg_dbus_method_invocation_return_valueshell_org_gtk_application_complete_openshell_org_gtk_application_complete_command_lineshell_org_gtk_application_proxy_get_typeshell_org_gtk_application_proxy_newg_async_initable_new_asyncshell_org_gtk_application_proxy_new_finishg_async_result_get_source_objectg_async_initable_new_finishshell_org_gtk_application_proxy_new_syncg_initable_newshell_org_gtk_application_proxy_new_for_busshell_org_gtk_application_proxy_new_for_bus_finishshell_org_gtk_application_proxy_new_for_bus_syncshell_org_gtk_application_skeleton_get_typeshell_org_gtk_application_skeleton_newshell_net_hadess_switcheroo_control_interface_infoshell_net_hadess_switcheroo_control_override_propertiesshell_net_hadess_switcheroo_control_get_typeshell_net_hadess_switcheroo_control_get_has_dual_gpushell_net_hadess_switcheroo_control_set_has_dual_gpushell_net_hadess_switcheroo_control_get_num_gpusshell_net_hadess_switcheroo_control_set_num_gpusshell_net_hadess_switcheroo_control_get_gpusshell_net_hadess_switcheroo_control_dup_gpusg_object_getshell_net_hadess_switcheroo_control_set_gpusshell_net_hadess_switcheroo_control_proxy_get_typeshell_net_hadess_switcheroo_control_proxy_newshell_net_hadess_switcheroo_control_proxy_new_finishshell_net_hadess_switcheroo_control_proxy_new_syncshell_net_hadess_switcheroo_control_proxy_new_for_busshell_net_hadess_switcheroo_control_proxy_new_for_bus_finishg_dbus_proxy_get_cached_property_namesg_strv_containsg_dbus_proxy_get_interface_nameg_dbus_proxy_get_object_pathg_dbus_proxy_get_nameg_dbus_proxy_get_connectionshell_net_hadess_switcheroo_control_proxy_new_for_bus_syncshell_net_hadess_switcheroo_control_skeleton_get_typeshell_net_hadess_switcheroo_control_skeleton_newgnome_shell_plugin_get_typeshell_app_get_typeg_param_spec_stringg_icon_get_typeg_param_spec_objectg_action_group_get_typeg_desktop_app_info_get_typeg_signal_connect_datashell_app_get_idg_hash_table_lookupg_timeout_add_secondsg_source_set_name_by_idshell_app_get_icong_app_info_get_icong_themed_icon_newshell_app_get_nameg_app_info_get_namemeta_window_get_wm_classg_dpgettextshell_app_get_descriptiong_app_info_get_descriptionshell_app_is_window_backedshell_app_create_icon_texturest_icon_newst_icon_set_icon_sizest_icon_set_fallback_icon_nameg_object_bind_propertyst_widget_add_style_class_nameshell_app_update_window_actionsmeta_window_get_gtk_window_object_pathg_object_get_datagtk_action_muxer_insertmeta_window_get_gtk_unique_bus_nameg_dbus_action_group_getg_object_set_data_fullshell_app_can_open_new_windowg_action_group_has_actiong_desktop_app_info_has_keyg_desktop_app_info_list_actionsmeta_window_get_gtk_application_object_pathmeta_window_get_gtk_application_idg_desktop_app_info_get_booleanshell_app_get_stateshell_app_get_n_windowsg_slist_lengthshell_app_is_on_workspacemeta_workspace_index_shell_app_new_shell_app_set_app_infog_utf8_collate_keyg_value_get_objectshell_app_get_busyg_value_set_stringshell_app_activate_action_finishg_task_get_typeshell_app_activate_actiong_async_result_is_taggedg_task_propagate_booleanshell_app_get_app_infoshell_app_update_app_actionsshell_app_compare_by_nameshell_app_system_get_type_shell_app_system_notify_app_state_changedg_warn_messageg_hash_table_removeshell_app_system_get_runningg_slist_sortshell_app_system_searchg_desktop_app_info_searchg_utf8_validateshell_app_usage_get_typeshell_app_usage_compareshell_blur_effect_get_typeshell_blur_effect_newshell_blur_effect_get_radiusshell_blur_effect_set_radiusclutter_effect_queue_repaintshell_blur_effect_get_brightnessshell_blur_effect_set_brightnessshell_blur_effect_get_modeshell_blur_effect_set_modeg_value_get_enumg_value_get_floatshell_camera_monitor_get_typeshell_global_get_type_shell_global_initg_object_new_valistshell_global_get_shell_global_destroy_gjs_contextshell_global_set_stage_input_regionmeta_is_wayland_compositorg_malloc_nXFixesCreateRegionmeta_display_get_x11_displaymeta_x11_display_set_stage_input_regionXFixesDestroyRegionshell_app_get_windowsmeta_window_is_override_redirectg_slist_reversemeta_display_get_workspace_managermeta_workspace_manager_get_active_workspaceg_slist_sort_with_datashell_app_activate_windowg_slist_findmeta_display_get_last_user_timemeta_display_xserver_time_is_beforeg_slist_copymeta_window_raise_and_make_recent_on_workspaceg_slist_freemeta_window_foreach_transientmeta_display_sort_windows_by_stackingmeta_window_get_window_typemeta_workspace_activate_with_focusmeta_window_set_demands_attentionmeta_window_activateshell_app_get_pidsmeta_window_get_pidshell_app_request_quitmeta_window_can_closemeta_window_deleteg_action_group_get_action_parameter_typeg_action_group_activate_actionmeta_context_restore_rlimit_nofileshell_app_launch_actiong_desktop_app_info_launch_actiong_application_id_is_validg_variant_is_of_typeg_cancellable_get_typeg_task_newg_task_set_source_tagg_task_get_nameg_app_launch_context_get_startup_notify_idg_variant_new_stringg_variant_new_take_stringg_task_set_task_datag_bus_getg_task_set_static_nameshell_app_launchsd_journal_stream_fdg_desktop_app_info_launch_uris_as_manager_with_fdsg_variant_get_child_valueg_variant_lookup_valueg_variant_get_strvg_app_launch_context_setenvshell_app_activate_fullg_dgettextshell_app_activateshell_app_open_new_windowg_app_info_should_showg_desktop_app_info_get_filenameg_app_info_get_executableg_app_info_get_commandlineg_app_info_get_display_nameg_icon_equalshell_app_system_lookup_appshell_app_system_lookup_heuristic_basenameshell_app_system_lookup_desktop_wmclassg_ascii_strdowng_strdelimitshell_app_system_lookup_startup_wmclassg_ptr_array_newg_timeout_addg_hash_table_remove_allg_str_has_prefixg_desktop_app_info_get_startup_wm_classg_str_equalg_ptr_array_find_with_equal_funcg_hash_table_foreach_removeg_hash_table_foreachg_ptr_array_foreachg_ptr_array_freeg_str_hashshell_app_system_get_installedshell_app_system_get_defaultmeta_window_is_skip_taskbar_shell_app_add_windowg_signal_connect_objectg_slist_nth_datameta_window_is_on_all_workspacesmeta_window_change_workspace_by_indexg_bus_get_syncgtk_action_muxer_new_shell_app_new_for_windowmeta_window_get_stable_sequence_shell_app_remove_windowg_slist_removeg_signal_handlers_disconnect_matchedg_signal_handler_disconnectg_desktop_app_info_get_generic_name_shell_app_handle_startup_sequencemeta_startup_sequence_get_completedmeta_startup_sequence_get_workspacemeta_startup_sequence_get_timestampmeta_display_unset_input_focusg_file_replaceg_buffered_output_stream_newg_data_output_stream_newg_ascii_dtostrg_output_stream_close_asyncshell_app_usage_get_most_usedg_dbus_proxy_new_syncg_settings_get_booleanshell_app_usage_get_defaultmeta_backend_get_typemeta_context_get_typemeta_display_get_typemeta_compositor_get_typemeta_workspace_manager_get_typeclutter_actor_get_typeg_settings_get_typest_focus_manager_get_typeg_file_get_typemeta_get_top_window_group_for_displaymeta_get_window_group_for_displaymeta_display_get_sizest_theme_context_get_for_stagemeta_settings_get_ui_scaling_factormeta_display_set_cursorfcntl64g_ascii_tableg_variant_lookupstrstrgnome_start_systemd_scopeg_file_delete_finishg_bytes_get_datag_file_replace_contentsg_bytes_refg_bytes_unrefg_task_run_in_threadshell_util_touch_file_asyncg_file_get_childg_variant_get_datag_variant_refg_variant_get_sizeg_bytes_new_with_free_funcg_file_delete_asyncg_file_get_pathg_mapped_file_newg_mapped_file_get_bytesg_variant_new_from_bytesg_mapped_file_unrefg_file_error_quarkcogl_pipeline_set_blendclutter_offscreen_effect_get_typecogl_snippet_set_replacecogl_pipeline_add_layer_snippetclutter_text_get_typeg_cclosure_marshal_VOID__VOIDgcr_prompt_get_typeg_type_check_value_holdsg_task_propagate_pointerg_task_propagate_intclutter_text_get_textg_array_unrefg_array_refg_strdupvg_mount_operation_get_typestrchrg_queue_push_tailmemcpyg_string_newg_string_free_and_stealg_string_insert_cg_string_insert_leng_string_append_leng_output_stream_write_allpolkit_agent_listener_get_typepolkit_unix_user_get_typepolkit_unix_user_get_uidgetpwuid_rg_idle_addg_list_copyg_list_foreachg_cancellable_connectg_list_appendg_list_lengthg_list_removeg_cancellable_disconnectdcgettextpolkit_error_quarkg_task_return_new_errorg_list_freeclutter_color_freeg_byte_array_appendst_image_content_new_with_preferred_sizeclutter_image_set_datag_task_return_pointermtk_rectangle_get_typecairo_surface_destroyg_date_time_unrefcairo_image_surface_get_heightcairo_image_surface_get_widthg_date_time_formatgdk_pixbuf_save_to_streamgdk_pixbuf_save_to_stream_finishshell_screenshot_composite_to_streamcogl_sub_texture_newcairo_image_surface_createcairo_image_surface_get_datacairo_image_surface_get_stridecogl_texture_get_datacairo_surface_mark_dirtycairo_surface_set_device_scalecairo_createcairo_set_source_surfacecairo_paintcairo_destroyg_date_time_new_now_localgdk_pixbuf_save_to_stream_asyncgcr_secure_memory_strfreeg_utf8_offset_to_pointermemmoveclutter_text_buffer_emit_inserted_textgcr_secure_memory_reallocg_utf8_find_prev_charg_utf8_strlenclutter_text_buffer_emit_deleted_textclutter_text_buffer_get_typeclutter_actor_get_preferred_heightst_bin_get_typest_widget_get_theme_nodest_theme_node_adjust_for_heightclutter_actor_get_first_childclutter_actor_get_next_siblingclutter_actor_get_preferred_widthst_theme_node_adjust_preferred_widthst_theme_node_adjust_for_widthst_theme_node_adjust_preferred_heightclutter_actor_set_allocationst_theme_node_get_content_boxclutter_actor_allocatest_widget_get_typest_widget_get_can_focusclutter_actor_containsclutter_actor_is_mappedclutter_actor_get_last_childclutter_actor_get_previous_siblingclutter_actor_is_visiblest_widget_navigate_focusclutter_actor_grab_key_focusg_object_class_install_propertyg_value_set_uintclutter_actor_box_get_widthclutter_actor_box_get_heightclutter_clone_get_typeclutter_clone_set_sourceclutter_color_get_typeg_param_spec_boxedg_value_set_boxedg_signal_stop_emission_by_nameg_file_get_parentg_file_make_directory_with_parentsg_file_createg_output_stream_closeg_dbus_connection_signal_unsubscribeg_task_get_completedg_task_return_error_if_cancelledgetpidsd_pid_get_user_unitg_dbus_connection_signal_subscribeg_strerrorg_io_error_from_errnoclutter_actor_iter_initclutter_actor_allocate_available_sizeclutter_actor_iter_nextclutter_actor_box_get_typemeta_window_get_buffer_rectclutter_actor_get_preferred_sizeclutter_actor_get_fixed_positionclutter_actor_allocate_preferred_sizeclutter_actor_remove_childclutter_layout_manager_get_typemeta_window_get_frame_rectmtk_rectangle_unionclutter_actor_box_equalclutter_layout_manager_layout_changedclutter_actor_destroyshell_global_get_contextshell_global_get_stageshell_global_get_displaymeta_enable_unredirect_for_displayshell_global_get_workspace_manager_shell_global_get_gjs_contextshell_global_notify_errorg_signal_emit_by_nameshell_global_get_pointermeta_cursor_tracker_get_for_displaymeta_cursor_tracker_get_pointershell_global_get_switcheroo_controlshell_global_get_settingsshell_global_get_current_timemeta_display_get_current_timeclutter_get_current_event_timeshell_global_reexec_selfg_file_get_contentsmeta_display_closeexecvp__errno_locationshell_global_create_app_launch_contextmeta_display_get_startup_notificationmeta_startup_notification_create_launchermeta_launch_context_set_timestampmeta_workspace_manager_get_workspace_by_indexmeta_launch_context_set_workspaceshell_global_begin_workshell_global_end_workg_idle_add_fullshell_global_run_at_leisureg_slist_appendshell_global_set_runtime_stateshell_global_get_runtime_stateshell_global_set_persistent_stateshell_global_get_persistent_state_shell_global_locate_pointer_shell_global_notify_shutdownshell_glsl_effect_get_typeshell_glsl_effect_add_glsl_snippetshell_glsl_effect_get_uniform_locationshell_glsl_effect_set_uniform_floatcogl_pipeline_set_uniform_floatshell_glsl_effect_set_uniform_matrixcogl_pipeline_set_uniform_matrixshell_invert_lightness_effect_get_typeshell_invert_lightness_effect_newshell_keyring_prompt_get_typeshell_keyring_prompt_newshell_keyring_prompt_get_password_actorshell_keyring_prompt_get_confirm_actorshell_keyring_prompt_completegcr_prompt_set_warningg_task_return_intshell_keyring_prompt_cancelgcr_prompt_closeshell_mount_operation_get_typeshell_mount_operation_newshell_mount_operation_get_show_processes_pidsshell_mount_operation_get_show_processes_choicesshell_mount_operation_get_show_processes_messageshell_perf_log_get_typeshell_perf_log_get_defaultshell_perf_log_set_enabledshell_perf_log_define_eventg_hash_table_newg_queue_newg_get_monotonic_timeshell_perf_log_eventcogl_flushcogl_get_proc_addressshell_perf_log_event_ishell_perf_log_event_xshell_perf_log_event_sshell_perf_log_define_statisticshell_perf_log_update_statistic_ishell_perf_log_update_statistic_xshell_perf_log_add_statistics_callbackshell_perf_log_collect_statisticsshell_perf_log_replayg_value_set_int64shell_perf_log_dump_eventsg_string_append_printfshell_perf_log_dump_logg_propagate_errorshell_polkit_authentication_agent_get_typeshell_polkit_authentication_agent_registerpolkit_unix_session_new_for_process_syncpolkit_agent_listener_registerg_error_newshell_polkit_authentication_agent_newshell_polkit_authentication_agent_unregisterpolkit_agent_listener_unregistershell_polkit_authentication_agent_completeshell_qr_code_generator_get_typeshell_qr_code_generator_generate_qr_codeg_task_report_new_errorclutter_color_copyshell_qr_code_generator_generate_qr_code_finishshell_qr_code_generator_newshell_screenshot_get_typeshell_screenshot_screenshot_areag_output_stream_get_typemeta_disable_unredirect_for_displayclutter_actor_queue_redrawshell_screenshot_screenshotshell_screenshot_screenshot_stage_to_contentshell_screenshot_pick_colorshell_screenshot_screenshot_finishshell_screenshot_screenshot_stage_to_content_finishshell_screenshot_screenshot_area_finishshell_screenshot_screenshot_window_finishshell_screenshot_screenshot_windowshell_screenshot_pick_color_finishcairo_image_surface_get_formatshell_screenshot_composite_to_stream_finishshell_screenshot_newshell_secure_text_buffer_get_typeshell_secure_text_buffer_newshell_keyring_prompt_set_password_actorclutter_text_set_buffershell_keyring_prompt_set_confirm_actorg_value_dup_stringshell_square_bin_get_typeshell_stack_get_typeshell_tray_icon_get_typeshell_tray_manager_get_typeshell_tray_manager_newshell_util_set_hidden_from_pickg_object_set_datashell_util_get_week_startnl_langinfoshell_util_translate_time_stringnewlocaleuselocalefreelocaleshell_util_regex_escapeg_regex_escape_stringshell_write_string_to_streamshell_get_file_contents_utf8_syncshell_util_touch_file_finishshell_util_wifexitedshell_util_create_pixbuf_from_datagdk_pixbuf_new_from_datashell_util_check_cloexec_fdsshell_util_get_uidgetuidshell_util_start_systemd_unitshell_util_start_systemd_unit_finishshell_util_stop_systemd_unitshell_util_stop_systemd_unit_finishshell_util_systemd_unit_existsshell_util_systemd_unit_exists_finishshell_util_sd_notifyshell_util_has_x11_display_extensionmeta_x11_display_get_xdisplayXQueryExtensionshell_window_preview_get_typeshell_window_preview_layout_get_typeshell_window_preview_layout_add_windowmeta_window_get_typemeta_window_get_compositor_privateclutter_clone_newclutter_actor_add_childshell_window_preview_layout_remove_windowshell_window_preview_layout_get_windowsshell_window_tracker_get_typeshell_global_get_window_trackershell_window_tracker_get_window_appshell_global_get_window_actorsmeta_get_window_actorsmeta_window_actor_is_destroyedg_list_reverseshell_global_get_session_modeshell_global_get_debug_flagsshell_global_set_debug_flags_shell_global_set_pluginmeta_plugin_get_displaymeta_display_get_contextmeta_context_get_backendmeta_display_get_compositormeta_get_stage_for_displayst_entry_set_cursor_funcmeta_display_get_selectionst_clipboard_set_selectionclutter_threads_add_repaint_func_fullmeta_backend_get_settingsst_focus_manager_get_for_stageshell_global_get_app_systemshell_global_get_app_cacheshell_global_get_app_usageg_cancellable_set_error_if_cancelledg_byte_array_sized_newg_byte_array_stealg_byte_array_unrefmeta_display_get_focus_windowclutter_actor_get_positionmeta_window_actor_get_imagemeta_window_frame_rect_to_client_rectmeta_window_get_client_typemeta_cursor_tracker_get_spritemtk_region_create_rectanglemtk_region_contains_pointmtk_region_unrefmeta_cursor_tracker_get_hotcairo_image_surface_create_for_datacairo_surface_get_device_scalemeta_display_get_monitor_index_for_rectmeta_display_get_monitor_scaleclutter_actor_get_resource_scaleshell_tray_manager_unmanage_screeng_object_remove_weak_pointershell_util_spawn_async_with_pipes_and_fdsg_spawn_async_with_pipes_and_fdsshell_util_spawn_async_with_pipesshell_util_spawn_asyncshell_util_spawn_async_with_fdsroundfmeta_x11_display_lookup_xwindowclutter_actor_set_opacityshell_tray_icon_newclutter_actor_queue_relayoutshell_tray_icon_clickst_theme_node_get_icon_colorsshell_tray_manager_manage_screeng_object_add_weak_pointerg_object_ref_sinkshell_util_get_translated_folder_namemeta_startup_sequence_get_typemeta_window_get_transient_formeta_window_is_remotemeta_window_get_sandboxed_app_idmeta_window_get_wm_class_instancemeta_window_get_startup_idmeta_startup_sequence_get_idmeta_startup_sequence_get_application_idg_path_get_basenamemeta_window_x11_get_groupmeta_group_list_windowsg_direct_equalg_direct_hashmeta_display_list_all_windowsg_hash_table_get_keysg_hash_table_sizemeta_window_actor_get_typemeta_size_change_get_typemeta_key_binding_get_typemeta_close_dialog_get_typemeta_inhibit_shortcuts_dialog_get_typeg_param_spec_doubleg_value_set_double__assert_failmemsetnm_connection_get_typenm_secret_agent_error_quarknm_secret_agent_old_delete_secretsnm_setting_connection_get_typenm_connection_get_settingnm_setting_connection_get_uuidsecret_password_clearnm_secret_agent_old_get_typeg_variant_dict_unrefnm_setting_get_secret_flagssecret_service_search_finishsecret_item_get_secretsecret_item_get_attributessecret_value_unrefnm_connection_update_secretsg_variant_dict_newsecret_value_getg_hash_table_replacenm_setting_connection_get_connection_typenm_connection_get_setting_by_namenm_setting_enumerate_valuesnm_connection_get_uuidsecret_attributes_buildsecret_service_searchnm_setting_wireless_get_typenm_setting_wireless_security_get_typenm_setting_802_1x_get_typenm_setting_wired_get_typenm_setting_pppoe_get_typenm_vpn_plugin_info_new_search_filenm_setting_get_namenm_setting_connection_get_idsecret_password_storevnm_setting_vpn_get_typenm_setting_vpn_get_service_typenm_connection_get_idnm_setting_vpn_foreach_secretnm_connection_for_each_setting_valuesecret_password_clear_finishg_dir_openg_dir_read_nameg_hash_table_containsg_key_file_newg_key_file_load_from_fileg_key_file_unrefg_dir_closeg_key_file_get_locale_stringg_get_system_data_dirsmtk_x11_error_trap_pushXGetWindowPropertymtk_x11_error_trap_popXFreemeta_x11_display_get_typemeta_x11_display_add_event_funcXInternAtomXChangePropertyXGetSelectionOwnermeta_x11_display_remove_event_funcXDestroyWindowXSetSelectionOwnerXReparentWindowXSendEventXMoveResizeWindowXMapWindowXGetWMNormalHintsXSelectInputmtk_x11_error_trap_pop_with_returnXFixesChangeSaveSetXGetWindowAttributesXGetVisualInfometa_x11_display_get_xrootXCreateWindowXUnmapWindowXShapeQueryExtensionXShapeSelectInputXShapeCombineRectanglesXQueryTreeshell_wm_get_type_shell_wm_switch_workspaceshell_wm_completed_switch_workspacemeta_plugin_switch_workspace_completedshell_wm_completed_minimizemeta_plugin_minimize_completedshell_wm_completed_unminimizemeta_plugin_unminimize_completedshell_wm_completed_size_changemeta_plugin_size_change_completedshell_wm_completed_mapmeta_plugin_map_completedshell_wm_completed_destroymeta_plugin_destroy_completedshell_wm_complete_display_changemeta_plugin_complete_display_change_shell_wm_kill_switch_workspace_shell_wm_kill_window_effects_shell_wm_show_tile_preview_shell_wm_hide_tile_preview_shell_wm_show_window_menu_for_rect_shell_wm_show_window_menu_shell_wm_minimize_shell_wm_unminimize_shell_wm_size_changed_shell_wm_size_change_shell_wm_map_shell_wm_destroy_shell_wm_filter_keybinding_shell_wm_confirm_display_change_shell_wm_create_close_dialog_shell_wm_create_inhibit_shortcuts_dialogshell_wm_newshell_workspace_background_get_typeqrcodegen_getSizeqrcodegen_encodeSegmentsAdvancedqrcodegen_encodeBinaryqrcodegen_encodeSegmentsqrcodegen_getModuleqrcodegen_isAlphanumericqrcodegen_isNumericqrcodegen_calcSegmentBufferSizeqrcodegen_makeBytesqrcodegen_makeNumericqrcodegen_makeAlphanumericqrcodegen_encodeTextqrcodegen_makeEcishell_network_agent_get_typeshell_network_agent_search_vpn_pluginshell_network_agent_add_vpn_secretshell_network_agent_set_passwordg_variant_dict_insertshell_network_agent_respondg_variant_dict_insert_valueg_variant_dict_endnm_simple_connection_new_clonenm_secret_agent_old_save_secretsshell_network_agent_search_vpn_plugin_finishshell_app_cache_get_typeg_app_info_get_allg_file_monitor_directoryg_file_monitor_set_rate_limitg_ptr_array_new_with_free_funcg_app_info_monitor_getshell_app_cache_get_allshell_app_cache_get_infoshell_app_cache_translate_folderna_tray_manager_get_typena_tray_manager_newna_tray_manager_manageXDefaultRootWindowXCreateSimpleWindowXMatchVisualInfoXVisualIDFromVisualna_tray_manager_set_colorsclutter_color_equalna_xembed_get_typena_tray_child_get_typena_tray_child_newna_xembed_add_idna_xembed_get_plug_windowg_list_remove_linkg_list_free_1na_xembed_get_socket_windowna_xembed_set_root_positionna_xembed_get_x11_displayna_tray_child_get_titleg_strndupna_tray_child_get_wm_classXGetClassHintg_string_append_unicharna_tray_child_get_pidna_xembed_get_sizena_tray_child_emulate_eventclutter_event_typeclutter_event_get_timeclutter_event_get_stateclutter_event_get_key_codeclutter_event_get_buttonna_xembed_set_available_sizena_xembed_set_background_colorXSetWindowBackgroundXClearWindowcairo_surface_get_contentgdk_pixbuf_newcairo_surface_get_typecairo_set_operatorcairo_surface_flushcairo_surface_statusgdk_pixbuf_get_has_alphagdk_pixbuf_get_rowstridegdk_pixbuf_get_pixelscairo_surface_referenceopendirreaddir64strtoldirfdclosedirgetrlimit64sysconfshell_window_tracker_get_app_from_pidshell_window_tracker_get_startup_sequencesmeta_startup_notification_get_sequencesshell_window_tracker_get_defaultst_theme_context_get_scale_factorclutter_actor_box_interpolateclutter_stage_get_capture_final_sizeclutter_stage_paint_to_contentcogl_framebuffer_clear4fcogl_framebuffer_draw_textured_rectangleclutter_texture_content_new_from_texturemeta_cursor_tracker_get_scaleclutter_stage_get_view_atclutter_stage_paint_to_buffermeta_workspace_get_work_area_for_monitormeta_display_get_monitor_geometryshell_app_cache_get_defaultlibgnome-shell-menu.solibst-14.solibgio-2.0.so.0libgobject-2.0.so.0libglib-2.0.so.0libgdk_pixbuf-2.0.so.0libgjs.so.0libmutter-mtk-14.so.0libm.so.6libgraphene-1.0.so.0libX11.so.6libmutter-clutter-14.so.0libcairo.so.2libmutter-cogl-14.so.0libpolkit-agent-1.so.0libpolkit-gobject-1.so.0libgcr-4.so.4libsystemd.so.0libpipewire-0.3.so.0libnm.so.0libsecret-1.so.0libmutter-14.so.0libXfixes.so.3libgnome-desktop-4.so.2libXext.so.6libc.so.6libshell-14.soGLIBC_2.2.5LIBSYSTEMD_209libnm_1_4_0libnm_1_0_0GLIBC_2.28GLIBC_2.4GLIBC_2.14GLIBC_2.3/usr/lib/x86_64-linux-gnu/mutter-14:/usr/lib/gnome-shell	
>w ui	�x�w ��b�xx0p�U�xp�U�xmx���
�xii
	�x����xii
�xui	�x@�,@�H�,��P�,�X�,p�h�,�1p�,�1��,�1��,�1��,r9ȴ,�1�,1�,11�,�1(�,-=0�,9=h�,;=p�,�1��,I=��,S=�,Y4�,R6(�,;=0�,�1h�,W=p�,MG��,\=��,�1�,;=�,�1�, �, �,�,(�,��,0�,`�,@�,��,H�,`�,P�, �,`�,�,��,c5��, �,��,�,��,1ȷ,P5з,@�,�,1�,75�,`�,(�,�0@�,�,H�,��,P�,��,h�,�&p�,�(��,�+ظ,&�,P�@�,�8H�,a=P�,i=X�,r=��,z=��,�=��,�=��,�=��,x`��,0Oȹ,��й, � �,�,(�,��,0�,`�,H�,M1`�, �,p�,�=��,�,��,�<��,����,��,,1�,11(�,x6H�,31P�,@�,`�,��,p�,�=��,�=��,�=��,�`��,�=��,�`��,>�,>�,*>�,�`�,1>�,B> �,|>0�,a8�,^>H�,HaP�,v>`�,pah�,�>��,�>��,5C��,�>ȼ,�>�,�>�,�> �,�>(�,?8�,?@�,;?h�,C?p�,[?��,c?��,<9��,|?��,E9Ƚ,�?н,�?�,�?�,�?��,�?�,�?�,�?�,@(�,�a0�,@@�,�aH�, @X�,�a`�,-@p�,�ax�,:@��,H@��,`@��,f@��,|@�,(x�,M�,�M�,^M�,�, �,��0�,,8�,p,`�,���,��H�,P�,X�,%`�,zh�,�p�,�x�,���,���,���,���,���,'��,���,���,���,���,��,%��,?��,E��,W��,��,��,��,��,�,�,�,	�,
 �,(�,0�,
8�,@�,H�,P�,X�,`�,h�,p�,x�,��,��,��,��,��,��,��,��,��, ��,!��,"��,#��,$��,&��,'��,(�,)�,*�,+�,, �,-(�,.0�,/8�,0@�,1H�,2P�,3X�,4`�,5h�,6p�,7x�,8��,9��,:��,;��,<��,=��,>��,?��,@��,A��,B��,C��,D��,E��,F��,G��,H�,I�,J�,K�,L �,M(�,N0�,O8�,P@�,QH�,RP�,SX�,T`�,Uh�,Vp�,Wx�,X��,Y��,Z��,[��,\��,]��,^��,_��,`��,a��,b��,c��,d��,e��,f��,g��,h�,i�,j�,k�,l �,m(�,n0�,o8�,p@�,qH�,rP�,sX�,t`�,uh�,vp�,wx�,x��,y��,{��,|��,}��,~��,��,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,��,��,��,��,��,��,��,��,��,	��,
��,�,�,
�,�, �,(�,0�,8�,@�,H�,P�,X�,`�,h�,p�,x�,��,��,��,��,��, ��,!��,"��,#��,$��,%��,&��,'��,(��,)��,*��,+�,,�,-�,.�,/ �,0(�,10�,28�,3@�,4H�,5P�,6X�,7`�,8h�,9p�,:x�,;��,<��,=��,>��,?��,@��,A��,B��,C��,D��,E��,F��,G��,H��,I��,J��,K�,L�,M�,N�,O �,P(�,Q0�,R8�,S@�,TH�,UP�,VX�,W`�,Xh�,Yp�,Zx�,[��,\��,]��,^��,_��,`��,a��,b��,c��,d��,e��,f��,g��,h��,i��,j��,k�,l�,m�,n�,o �,p(�,q0�,r8�,s@�,tH�,uP�,vX�,w`�,xh�,yp�,zx�,{��,|��,}��,~��,��,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,��,��,��,��,��,��,��,��,��,	��,
��,��,��,
��,��,��,�,�,�,�, �,(�,0�,8�,@�,H�,P�,X�,`�,h�,p�,x�, ��,!��,"��,#��,$��,%��,&��,(��,)��,*��,+��,,��,-��,.��,/��,0��,1�,2�,3�,4�,5 �,6(�,70�,88�,9@�,:H�,;P�,<X�,=`�,>h�,?p�,@x�,A��,B��,C��,D��,E��,F��,G��,H��,I��,J��,K��,L��,M��,N��,O��,P��,Q�,R�,S�,T�,U �,V(�,W0�,X8�,Y@�,ZH�,[P�,\X�,]`�,^h�,_p�,`x�,a��,b��,c��,d��,e��,f��,g��,h��,i��,j��,k��,l��,m��,n��,o��,p��,q�,r�,s�,t�,u �,v(�,w0�,x8�,y@�,zH�,{P�,|X�,}`�,~h�,p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,�X�,�`�,�h�,�p�,�x�,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,���,��,��,��,��,� �,�(�,�0�,�8�,�@�,�H�,�P�,X�,`�,h�,p�,x�,��,��,��,	��,
��,��,��,
��,��,��,��,��,��,��,��,��,�,�,�,�, �,(�,0�,8�,@�,H�, P�,!X�,"`�,#h�,$p�,&x�,'��,(��,)��,*��,+��,,��,-��,.��,/��,0��,1��,2��,3��,4��,5��,6��,7�,8�,9�,:�,; �,<(�,=0�,>8�,@@�,AH�,BP�,CX�,D`�,Fh�,Gp�,Hx�,I��,J��,K��,L��,M��,N��,O��,P��,Q��,R��,S��,T��,U��,V��,X��,Y��,Z�,[�,\�,]�,^ �,_(�,`0�,a8�,b@�,c��H��H��o+H��t��H����5�T+�%�T+@��h���f���h����f���h����f���h���f���h���f���h���f���h���f���h�r���f���h�b���f���h	�R���f���h
�B���f���h�2���f���h�"���f���h
����f���h����f���h��f���h���f���h����f���h����f���h���f���h���f���h���f���h���f���h�r���f���h�b���f���h�R���f���h�B���f���h�2���f���h�"���f���h����f���h����f���h��f���h ���f���h!����f���h"����f���h#���f���h$���f���h%���f���h&���f���h'�r���f���h(�b���f���h)�R���f���h*�B���f���h+�2���f���h,�"���f���h-����f���h.����f���h/��f���h0���f���h1����f���h2����f���h3���f���h4���f���h5���f���h6���f���h7�r���f���h8�b���f���h9�R���f���h:�B���f���h;�2���f���h<�"���f���h=����f���h>����f���h?��f���h@���f���hA����f���hB����f���hC���f���hD���f���hE���f���hF���f���hG�r���f���hH�b���f���hI�R���f���hJ�B���f���hK�2���f���hL�"���f���hM����f���hN����f���hO��f���hP���f���hQ����f���hR����f���hS���f���hT���f���hU���f���hV���f���hW�r���f���hX�b���f���hY�R���f���hZ�B���f���h[�2���f���h\�"���f���h]����f���h^����f���h_��f���h`���f���ha����f���hb����f���hc���f���hd���f���he���f���hf���f���hg�r���f���hh�b���f���hi�R���f���hj�B���f���hk�2���f���hl�"���f���hm����f���hn����f���ho��f���hp���f���hq����f���hr����f���hs���f���ht���f���hu���f���hv���f���hw�r���f���hx�b���f���hy�R���f���hz�B���f���h{�2���f���h|�"���f���h}����f���h~����f���h��f���h����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r���f���h��b���f���h��R���f���h��B���f���h��2���f���h��"���f���h�����f���h�����f���h���f���h����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r���f���h��b���f���h��R���f���h��B���f���h��2���f���h��"���f���h�����f���h�����f���h���f���h����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r���f���h��b���f���h��R���f���h��B���f���h��2���f���h��"���f���h�����f���h�����f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h���f���h���f���h���f���h��f���h��f���h��f���h��f���h�r�f���h�b�f���h	�R�f���h
�B�f���h�2�f���h�"�f���h
��f���h��f���h���f���h���f���h���f���h���f���h��f���h��f���h��f���h��f���h�r�f���h�b�f���h�R�f���h�B�f���h�2�f���h�"�f���h��f���h��f���h���f���h ���f���h!���f���h"���f���h#��f���h$��f���h%��f���h&��f���h'�r�f���h(�b�f���h)�R�f���h*�B�f���h+�2�f���h,�"�f���h-��f���h.��f���h/���f���h0���f���h1���f���h2���f���h3��f���h4��f���h5��f���h6��f���h7�r�f���h8�b�f���h9�R�f���h:�B�f���h;�2�f���h<�"�f���h=��f���h>��f���h?���f���h@���f���hA���f���hB���f���hC��f���hD��f���hE��f���hF��f���hG�r�f���hH�b�f���hI�R�f���hJ�B�f���hK�2�f���hL�"�f���hM��f���hN��f���hO���f���hP���f���hQ���f���hR���f���hS��f���hT��f���hU��f���hV��f���hW�r�f���hX�b�f���hY�R�f���hZ�B�f���h[�2�f���h\�"�f���h]��f���h^��f���h_���f���h`���f���ha���f���hb���f���hc��f���hd��f���he��f���hf��f���hg�r�f���hh�b�f���hi�R�f���hj�B�f���hk�2�f���hl�"�f���hm��f���hn��f���ho���f���hp���f���hq���f���hr���f���hs��f���ht��f���hu��f���hv��f���hw�r�f���hx�b�f���hy�R�f���hz�B�f���h{�2�f���h|�"�f���h}��f���h~��f���h���f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h��r�f���h��b�f���h��R�f���h��B�f���h��2�f���h��"�f���h���f���h���f���h�����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h�r��f���h�b��f���h	�R��f���h
�B��f���h�2��f���h�"��f���h
���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h�r��f���h�b��f���h�R��f���h�B��f���h�2��f���h�"��f���h���f���h���f���h����f���h ����f���h!����f���h"����f���h#���f���h$���f���h%���f���h&���f���h'�r��f���h(�b��f���h)�R��f���h*�B��f���h+�2��f���h,�"��f���h-���f���h.���f���h/����f���h0����f���h1����f���h2����f���h3���f���h4���f���h5���f���h6���f���h7�r��f���h8�b��f���h9�R��f���h:�B��f���h;�2��f���h<�"��f���h=���f���h>���f���h?����f���h@����f���hA����f���hB����f���hC���f���hD���f���hE���f���hF���f���hG�r��f���hH�b��f���hI�R��f���hJ�B��f���hK�2��f���hL�"��f���hM���f���hN���f���hO����f���hP����f���hQ����f���hR����f���hS���f���hT���f���hU���f���hV���f���hW�r��f���hX�b��f���hY�R��f���hZ�B��f���h[�2��f���h\�"��f���h]���f���h^���f���h_����f���h`����f���ha����f���hb����f���hc���f���hd���f���he���f���hf���f���hg�r��f���hh�b��f���hi�R��f���hj�B��f���hk�2��f���hl�"��f���hm���f���hn���f���ho����f���hp����f���hq����f���hr����f���hs���f���ht���f���hu���f���hv���f���hw�r��f���hx�b��f���hy�R��f���hz�B��f���h{�2��f���h|�"��f���h}���f���h~���f���h����f���h�����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r��f���h��b��f���h��R��f���h��B��f���h��2��f���h��"��f���h����f���h����f���h�����f���h�����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r��f���h��b��f���h��R��f���h��B��f���h��2��f���h��"��f���h����f���h����f���h�����f���h�����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r��f���h��b��f���h��R��f���h��B��f���h��2��f���h��"��f���h����f���h����f���h�����f���h�����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r��f���h��b��f���h��R��f���h��B��f���h��2��f���h��"��f���h����f���h����f���h�����f���h�����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r��f���h��b��f���h��R��f���h��B��f���h��2��f���h��"��f���h����f���h����f���h�����f���h�����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r��f���h��b��f���h��R��f���h��B��f���h��2��f���h��"��f���h����f���h����f���h�����f���h�����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r��f���h��b��f���h��R��f���h��B��f���h��2��f���h��"��f���h����f���h����f���h�����f���h�����f���h�����f���h�����f���h����f���h����f���h����f���h����f���h��r��f���h��b��f���h��R��f���h��B��f���h��2��f���h��"��f���h����f���h����f���h�����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h�r��f���h�b��f���h	�R��f���h
�B��f���h�2��f���h�"��f���h
���f���h���f���h����f���h����f���h����f���h����f���h���f���h���f���h���f���h���f���h�r��f���h�b��f���h�R��f���h�B��f���h�2��f���h�"��f���h���f���h���f���h����f���h ����f���h!����f���h"����f���h#���f���h$���f���h%���f���h&���f���h'�r��f���h(�b��f���h)�R��f���h*�B��f���h+�2��f���h,�"��f���h-���f���h.���f���h/����f���h0����f���h1����f���h2����f���h3���f���h4���f���h5���f���h6���f���h7�r��f���h8�b��f���h9�R��f���h:�B��f���h;�2��f���h<�"��f���h=���f���h>���f���h?����f���h@����f���hA����f���hB����f���hC���f���hD���f���hE���f���hF���f���hG�r��f���hH�b��f���hI�R��f���hJ�B��f���hK�2��f���hL�"��f���hM���f����%6:+fD���%F:+fD���%F:+fD���%F:+fD���%F:+fD���%>:+fD���%>:+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%~
+fD���%v
+fD���%n
+fD���%f
+fD���%^
+fD���%V
+fD���%N
+fD���%F
+fD���%>
+fD���%6
+fD���%.
+fD���%&
+fD���%
+fD���%
+fD���%
+fD���%
+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%�
+fD���%~
+fD���%v
+fD���%n
+fD���%f
+fD���%^
+fD���%V
+fD���%N
+fD���%F
+fD���%>
+fD���%6
+fD���%.
+fD���%&
+fD���%
+fD���%
+fD���%
+fD���%
+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%�	+fD���%~	+fD���%v	+fD���%n	+fD���%f	+fD���%^	+fD���%V	+fD���%N	+fD���%F	+fD���%>	+fD���%6	+fD���%.	+fD���%&	+fD���%	+fD���%	+fD���%	+fD���%	+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%�+fD���%~+fD���%v+fD���%n+fD���%f+fD���%^+fD���%V+fD���%N+fD���%F+fD���%>+fD���%6+fD���%.+fD���%&+fD���%+fD���%+fD���%+fD���%+fD���%�+fD���%�+fD���%�+fD���%�+fD1�H�CHH�%��H�=%+�@���H�=+�@�H�=1+H�*+H9�tH�+H��t	�����H�=+H�5�+H)�H��H��?H��H�H�tH��+H��t��fD�����=�+u+UH�=Z+H��tH�=�+����d�����+]������w������H��H�G(���H�5�*�@��H��H�G(���H��,H��H�G H��fHn�fHn�fl�G���H��*�@��H�%H�^H�G H��fHn�fHn�fl�G���H���*�@���ff.��������H�G�f���UH��SH��H��H�8���H�{@���H��+H��H�]�H�@0��f���UH��SH��H��H�8H�C8H��t����H�{(H�C(H��t����H�{XH�CXH��t���H�{HH�CHH��t���H�{pH�CpH��t���H�{`H�C`H��t�l��H�{0H�C0H��t�V��H�{PH�CPH��t�@��H�{hH�ChH��t�*��H�++H��H�]�H�@0��D��UH��SH��H��H�GH��tH�WH�H�BH�C0H��tH�{��H�CHH��tH�S@H�H�BH�C`H��tH�{@H�]���DH�]���f.��������1����UH��SH��H�H������H��H�]���J��f.���UH��SH��H�H�����H��H�]�����f.�H��H	���UH��AUATSH��H��H��tTI��H��tL��L��A����A9�u7H�;H��tBE1�1��A�EH�<�I��H��H��t I�4������t�H��1�[A\A]]�fDH���[A\A]]����f.�UL�Q��1�H�
��H�5QH�����ff.�UH��AUATSH��L�/L;.��I�E�H��I��H��DwH� �Hc�H�>��fD��I9���L���:��H�t�1�H��1����1��fD����L�������8�����H��[A\A]]�fD���L������9���������{��L����q��9��������{�L����q�9��������;��L��H���0��H9������q���f���L��H����H9������Q���f�����L��fH~����fH~�H9������*���H�����H��I���u��L��H��������������DH���P��H��I���E��H��L	�t-H�������M�������H��L��[A\A]]�)��f�����fDL�����H��I���}���H��L��[H��A\A]]����L�rH�
:���1�H�5�N�W��UH��AUATSH��L�/L;.��I�E�H��I��H��DwH��Hc�H�>��fD�{�I9���L�����H��q�1�H��1��r��1��fD���L������8�����H��[A\A]]�fD����L������9���������;��L����1��9��������;�L����1�9����������L��H������H9������q���f����L��H����H9������Q���f����L��fH~��~��fH~�H9������*���H���@��H��I���5��L��H���J������������DH�����H��I�����H��L	�t-H�������M�������H��L��[A\A]]����f�����fDL���H�H��I���=�H��L��[H��A\A]]����L��oH�
"���1�H�5�L�����H��������UH��AWAVI��AUI��ATI��SL��H���G uGH����I�NI��I��H��tGI�t$ L��I�T$8A��H��L��[��A\A]A^A_]�x���L�H���T�I��H��H��u�M�N �H�@LH�5�1�����H�LH�5�1������H��������UH��AWAVI��AUM��ATI��SH��H���G uGH�����M�NM��I��M��tGH�s L��H�KPH�S8L�ChA��H��L��[��A\A]A^A_]���f�L�H����M��I��M��u�M�V �H��KH�5�1����H�CKH�5ڟ1�������H��������UH��AWAVI��AUM��ATI��SH��H���G uGH����M�NM��I��M��tGH�s L��H�KPH�S8L�ChA��H��L��[��A\A]A^A_]����f�L�H����M��I��M��u�M�V �H��JH�5�1��+��H�sJH�5ʞ1��������twUH��AUATI��S��H��H�H�8�z��I��I�$H�8�k��AUL�
aJ1�PA��1�H�
XJA�t$H�Lm�S����H�� H�e�[A\A]]�f.��wH�����DUH�=.JH��� ��H��L�
e���A�HjH�ƺ��PH�
Iv������f�UH�=�IH������H��L�
U�A�XjH�ƺ��PH�
I�����f�UH�=�IH�����H��L�
�EA��jH�ƺ��PH�
i!�T�����f�UH�=�IH���`��H��L�
�GA� jH�ƺ��PH�
��������f���UH��AVAUATSH������H��H�3E1�jTI�Ĺ�PH����L�5�*H�=&IjjM��P1����H��0�"�H��H�3M��jTI��E1ɹ j@�H�=�HPH�E���ATjjP1����H��8H�3M��jTE1ɹ�H���AUH�=�Hj@ATjjP1��~��H�5�HH��@1�H��H�=�MA�����H�e�H��H��[A\A]A^]���D��UH��SH��H�����U�*���mH�v�H���fHn�fHn�H�>�fl�H�
ÒfHn���fHn�H�ˑH�5��fl�fHn�H����fHn�H�a�H�
��fl�fHn�H����fHn�fHn�H�5b�fl�H��H�����fHn�fHn�H���fl�fHn�H�
8���fHn�H���H�=>�fl�fHn�H�ޗ��fHn�fHn�H�56�fl�H�����fHn�fHn�fl��fHn�fl��fHn�fl�� H�]���f�H�5�*H����������ff.����UH��SH��H����H��*��*��uH�3%H�C0H�]���f�H�5�*H��豾����ff.�@��UH��SH��H��H� �7��H���*H��H�]�H�@0��f���UH��SH��H��H� ���H�@�*H��H�]�H�@0��f���UH��ATS��u5H�5FI�����H��H��tL��H���.��H��[A\]����f�[A\]�L��hH�
ݘ��1�H�5�D�*�f.���UH�5cFH��SH�����H��H��tH��觹��H��H�]���ff.���UH��ATS��u~I��H�=WEH���ν��H��H������H�8EH�58EH��H��H�=?E1�����L��E1�1�H��H���*A�����PH��H�5�gP��XZH�e�H��[A\]����L��gH�
����1�H�5�C�1����U��H��AUATSH����wH���*I��H��L�,�I�}�
���H��H���2��I�UH�5�DH�=�DH��H��1��3��AUL��E1�H��H��1�A�����PH�5g��XZH�e�H��[A\A]]�H���L�gH�
J��C1�H�5%C�w����U��H��AUATSH����wbH���*I��L�,�I�u����H��A�E0u&H��t1L��H�����H��H��[A\A]]�ķ��@H��L���U���H��u�H��[A\A]]�L�vfH�
��1�H�5�B���@��H�5l]�p����UH��AWAVL�5M�*AUL�m�ATL�%hCSH��H��H�5�CH��8H�U�H�U�dH�%(H�E�1�����6fDH�u�L���<�H�}�I���0��H�{ 1�1҉�����M����H�}�1�1�L��L���%����u�H�}�����L�u�I�6H��tWE1�L�=��*f�L�����I�>I������H�{ 1�1҉�螸��M��t
I�t$(H���|��H�M�A�EI��L�4�I�6H��u�H�E�dH+%(u'H��8[A\A]A^A_]��I�w(H���4���G������f.���UH��AWAVL�5��*AUL�m�ATL�%BSH��H��H�5�BH��8H�U�H�U�dH�%(H�E�1��@���6fDH�u�L�����H�}�I������H�{ 1�1҉�豷��M����H�}�1�1�L��L��������u�H�}��ȶ��L�u�I�6H��tWE1�L�=R�*f�L����I�>I���}��H�{ 1�1҉��N���M��t
I�t$(H���,��H�M�A�EI��L�4�I�6H��u�H�E�dH+%(u'H��8[A\A]A^A_]��I�w(H�������G����j��f.�UH�=�@H��SH���+��H��L�
��A� jH�ƺ��PH�
�}�߷���(H��H��������*H��H�]�����UH��AUATSH��H�U�H��dH�%(H�E�1�H�E��\�H��t'H���/���H�E�dH+%(uVH�e�[A\A]]�DL�e�A�<$E�l$�~��H��M�D$1�H�KAUI���H��b1�跽��H�}�����XZ��U��D��UH��AUATSH��H�U�H��dH�%(H�E�1�H�E���H��t'H������H�E�dH+%(uVH�e�[A\A]]�DL�e�A�<$E�l$����H��M�D$1�H�KAUI���H�vb1�����H�}��^���XZ����D��UH�5�>H��ATE1�S���H��tH��H��覽��H��A���۲��D��[A\]���UH�5�>H��ATE1�S覿��H��tH��H���f���H��A��蛲��D��[A\]���Uf�L��H�=m�*H��AWAVM��AUM��ATSH��(L�}dH�%(H�E�1�H�E�)E��s��H����H�p(I�?I������H��H��tcL�m�H�pL���C�I�t$(L��L���s��I�|$�Y���L��H���~��L��H���C�H�E�dH+%(u6H��(H��[A\A]A^A_]�f�苲��M��L����H�
�=1��@���)��L��DH�
����1�H�5\<������Uf�L��H�=M�*H��AWAVM��AUM��ATSH��(L�}dH�%(H�E�1�H�E�)E��S��H����H�p(I�?I�����H��H��tcL�m�H�pL���#��I�t$(L��L���S��I�|$�9���L��H���^��L��H���#�H�E�dH+%(u6H��(H��[A\A]A^A_]�f��k���M��L����H�
�<1�� ���	��L��CH�
���1�H�5V;������Uf�L��H�=-�*H��AWAVAUM��ATM��SH��(L�}L�udH�%(H�E�1�H�E�)E��/��H����H�p(I�>H�����H��tbL�e��C0uHL��L�����H�s(L��L���~��L�����H�U�dH+%(uFH��([A\A]A^A_]�L��L���%�����K���M��L����H�
�;1���1������L��BH�
9���1�H�5:���fD��Uf�L��H�=
�*H��AWAVAUM��ATM��SH��(L�}L�udH�%(H�E�1�H�E�)E����H����H�p(I�>H���g��H��tbL�e��C0uHL��L���͸��H�s(L��L���^��L������H�U�dH+%(uFH��([A\A]A^A_]�L��L��������+���M��L����H�
�:1�����1������L�sAH�
��)1�H�59�f��fD��UH��AUATI��H�=�:SH��P���H��dH�%(H�E�1��7���H��H�����L�����L��I���I���H��L��E1�ATH�
�9H��1�L�v9����ZYH��t.I��H���Ӿ��H��L��1�H�P9H�5:�(��L���`���H���h���H�U�dH+%(uH�e�[A\A]]�����f���UH��AWL�=�*AVE1�AUATI��H�=�9SH���*H��dH�%(H�E�1��I���H��H��@���H��H��0�������� f�A�FI��I��H�����C�t�L�CL��L��8������L��I������H��L��E1�ATL��8���H��1�H�
e8��I��XZM��t�L��衽��H�S1�L��H��0���H�5�8���L���-���A�FI��I��H���m���f�H��0�������H�U�dH+%(uH�e�[A\A]A^A_]��q�����UH��SH��H��H�G H�x ����H�{ H�H�� �X�����H�]������UH��SH��H��H�G H�x ���H�{ H�H�� �X�`����H�]������UH��SH��H��H�G H�x �S��H�{ H�H�� �X � ����H�]������UH��SH��H��H�G H�x ���H�{ H�H�� H�X8����H��H�]���fD��UH��SH��H��H�G H�8�$��H�C H�8訪��H�C H�5-�H�x����H�C H�xH��t	���H�C H�x�5���H�C H�x �x���H���*H��H�]�H�@0����UH��SH��H��H�G H�8���H�C H�8H�����H�C H�8H��0���H�C H�8����H�C H�5��H�x�4��H�C H�xH��t	�r��H�C H�x蕮��H�C H�x �ج��H���*H��H�]�H�@0����UH��ATS��u3H�G H��I��H�x ���H�C L��H�8�^��H�{ [A\]H�� �m��L�XH�
��1�H�5@4���ff.����UH��AUATS�^�H����wCH�G I��I��H�x �4��I�D$ H�[L��H�H�<�����I�|$ H��[A\H�� A]]����L��WH�
����1�H�5�3�#�����GH�5v�*���f���U1���1�H��SH��H��4H���k���H��HǃH��t����H�5��*H��H�]���;��ff.���UH��ATI��SH�G H�x �E��I�\$ H�{tH�{tH�{ [A\]���D蛵��1�H�CI�D$ H�x�'���L��诧��H�
@�*H�5H��I�D$ H�x�@��I�D$ H�5dWH�x���I�D$ H�pH�x�ɿ��I�D$ H�x�;���I�\$ H�{ [A\]�y��f���UH��ATI��SH�G H�x �u��I�\$ H�{tH�{tH�{ [A\]�5��D�˴��1�H�CI�D$ H�x�W���L���ߦ��H�
p�*H�5�H��I�D$ H�x�p��I�D$ H�5�VH�x���I�D$ H�pH�x���I�D$ H�x�k���I�\$ H�{ [A\]���f���UH��ATSH��Hc=��*H�H�{ H�� �۵��L�c �b���I�D$�3���H��H�C H�8[A\]���ff.���UH��ATSH��Hc=�*H�H�{ H�� �{���L�c ����HI�D$�����H��H�C H�8�/��H�C �H�8H�����H�C �T[A\]H�8H��0������UH��AWAVAUATI��SH��(dH�%(H�E�H�G H�x ���H�=B2���H��H��@���H��H������趻��H�=�1�ʩ��H��H������H��H������葻��I�L$ L�yM����E1��f.�M�M��txI�L$ M�7A�F��H�4@H�H��I�vH�������u�I�A��H�x�M���H��H���r���H������H�5�1H��I�H��H�P1����H��踤��M�M��u�E����H������H������H�5t0H�=�01�L�=�T�,���H����L��I���y��H������I��H��t@f.�L��I��5���H��M��L��jH��L��01�H���%���M�vXZM��u�L������H�5c�*H�������/���I�D$ H�5s��H�x����I�|$ f�GH�� ����H�E�dH+%(u/H�e�1�[A\A]A^A_]�@H����������H������������!������UH��SH��H��H�G H�x 胿��H�{ H�GH��t.H������H�{ H�GH�� �9��H��H�]���l���@H�]�H�� ����f.���UH��AWAVAUATI��SH��(dH�%(H�E�H�G H�x ���H�=�/�?���H��H��@���H��H����������H�=./����H��H������H��H���������I�L$ L�yM����E1��f.�M�M��txI�L$ M�7A�F��H�4@H�H��I�vH���Q����u�I�A��H�x蝦��H��H���µ��H������H�5�.H��I�H��H�P1�����H������M�M��u�E����H������H������H�5�-H�=L.1�L�=�Q�|���H���D���L��I������H������I��H��t@f.�L��I�腾��H��M��L��jH��L��-1�H���u���M�vXZM��u�L���b���H�5��*H����������I�D$ H�5���H�x�j���I�|$ f�GH�� ����H�E�dH+%(u/H�e�1�[A\A]A^A_]�@H�������d���H�������X�����q������UH��SH��H��H�G H�x �Ӽ��H�{ H�GH��t.H���.��H�{ H�GH�� ���H��H�]���l���@H�]�H�� ��j���f.�UH��AWA��AVAUI��ATI��SH��H��H�G �
DH�H9t`H�@H��u�(����H�I�\$ H��I��D�xI��H�{�>���I�uL��H�C�.��H��L��L��[A\A]A^A_]���DH��[A\A]A^A_]Ð��UH��AWAVAUATD�f�SH��H�M�A����H�r�*H��A��I��N�<�H�G O�$dI��H�x 萻��H��踶��H�C L��H�0L��f����t*H�C H�x �E���H��H��[A\A]A^A_]����H�����H��tA�G0u$H�C L��H�0L�����H�u�H������@H�C D��L��H��H�L��x�����L��MH�
Hy�1�H�5�)���D��UH��AUATSH������H�G H��I��I��H�x 蠺��H���ȵ��H�C L��H�0�9����t%H�C H�x �X���H��H��[A\A]]����fDH���ؠ��H��tH�C �H�5�*H��H����H�C L��H�0����L��H������L��LH�
n{��1�H�5�(�;��ff.���UH�5*A��1�H��H��SH��H�=J*H��蒡��H��H������H��E1�1�h�H�5.*A�����H�=)*H���m���H��H���Ң��H�=*�ơ��H�5�)H�=*E1�H��H��A�����H��H�]�H��XZ�钢��f���UH�5�)H��ATE1�S����H��tH��H���V���H��A�����D��[A\]�UH�=�)H��SH���K���H������H��H��L�
u��jH��A�(�0H�
������H�]�����UH��SH��H�����H�L�*�>�*��uZH�#"H��L1�A��H�C0H�	��H�5)H�C H�=)�"���H��H�]�H���*H���*���2���f�H�5��*H�������ff.�@��UH��ATI��SH�>H��t����H9t[�A\]�DH�[L��H�;����H��[A\]�@��UH��AWAVI��AUI��ATSH��H��譣��L�{L��I��L9��E�藣��H;C��t�}�uxM9�t��t�H��[A\A]A^A_]�L���0���L�����&�����t2��t>��t*L���R���L����H���H���‰�[A\)�A]A^A_]���u���f.�������f���UH��AVAUATS�G;Ft���U���[A\A]A^]�H�W0I��I��H���H�ZH��u�.�H�[H����H�;�c�����t�I�E0H��t>H�XH����E1��@H�[H����H�;�+�����t�E���n���fDA�D$1ۃ��]���I�D$0I�U0H�xL�j�M��u�fDM�mM����I�}�
���9�r�I�}�������f�I�E0H����H�XA�H���]����s���A���f����!@H�V0H���X���H�ZE1�H���(�����������H�V0H�������H�ZA�H�����������@I�D$0H���o���L�`M���b���E1���M�d$M��tI�<$�-���D9�r�I�<$����A����D)��'���M����������H�B0H��t$H��H��H�PH9
t�H�5R�*1�1�鑨����UL�z%��1�H�
)rH�5N#H������f.���I�@0H��t�H�5�*L��1�1��=���UL�(%��1�H�
GqH�5�"H���x�������H�5+�*1��<���ff.����UH��ATI��SH�U�H��dH�%(H�E�1�H�E����H�u�H��H��tJL��H�E����H�}�H��t���H��tH��藗��H�E�dH+%(uH��[A\]���L���˰�����@��UH��H��AWAVH�u�AUATSH��H��dH�%(H�E�1�H�E��%��H�u�I��H��taH��H�E��9���1���1����H�}�H��t�-���M��tL����H�E�dH+%(�H�e�[A\A]A^A_]�fDH���0���H�x �g���H��蟦���.H��I���ϲ��I��H��tH�5~#H��註������1�L��H�=F%1��=����I�Ƅ�u�)��8-u�_�PH����t��.u��/�PH����u�H��萤��H��I��起��SL�#L��I��H����L��L��PH�
#AWj�jj�5���H��0L���ɕ��L�����H�}�H�����������A��?����:���f.���UH��SH��H���4��u9H�{(萕��H�{臕��H�{ �~���H���*H��H�]�H�@0��f��C4����f���UH��ATI��SH�_H�;����H�{���H�{���H�{H�5a�*�4����{ ��uH�F�*[L��A\]H�@0��f��C �D����f���UH��SH��H�_�Z���H���B����K$�Q��w��u�S$�H�]���DH�C 1�H�]���U1�H��AWAVI��AUI��ATI��H�=�!SH������1�L��L��H��H���դ��H��A���*���E��uH��1�[A\A]A^A_]�@L��H������q���1�L��L��H��H��莤��H��A�����E��t�H��L��L��1�[H�5!A\A]A^A_]�\���ff.����UH��AWM��AVM��AUI��H�5� ATL��I��SH��H��轸����tH�5� L��誸����uH��[A\A]A^A_]��H�5�8L��聸������H�H���L��H����8i���xd���x��H�9����H�����H�E�贿��I�~PH�u�H��I��衣��L�3M���_���L�-  �0@H�5 L���������L�sH��I��M���&���L��L�������u�I�<$1�肼���A���3���M��H�
���H��L���1�[A\A]A^A_]����f.�H�BH��H��H����������M��H�
�B����I�<$�
1�萿����I�G�D���DU1�H��AWAVI��AUATSH������H��H��XH�dH�%(H�E�1�HDž����躟��H������H��t^�u0�Ӗ��H�E�dH+%(�IH��X[A\A]A^A_]�fDH�O1�H��A�1��/���H�������fD1�L��1�I��H�=?�*L���������I���fDH��L��L���J�����t�I��1�L��L�����H��H���L��L�������j���1�1�L���.���L��L�������ϐ���*��I�vPL��H��H��4�ׂ�CH��H��?H��H)�H�����茣��@1�L��L��苪����t'H��������pf/v�H;X~�L������f�H������H�������H�H�����¬��f���H��H�׃�tIUH��AUATS��H����t��u@���H�e�[A\A]]����p|H�e�[A\A]]�.���fD����[��H�I��H�8�
���I��I�$H�8���AUL�
�1�PA��1�H�
A�t$H��=�S�Y���H�� H�e�[A\A]]�f.���UH��SH��H��H�a�*���H�{8H�C8H��t�=���H�{(H�C(H��t�'���H�{XH�CXH��t����H�{HH�CHH��t���H�{pH�CpH��t���H�{`H�C`H��t�ώ��H���G���H�CH�]���ff.�f�UH�=*H��SH�����H���S���H��H��L�
ujH��A����H�
N詑��H�]���H�=!�*H��t鿸���UH������H������H��蟗��1�H��H���*莨��H�=��*�&1��&薪��H�=��*�/�1����H�=��*]�V���fDU@��f���*�H��ATSL�e�H��L��H���^
�m�Z�(�dH�%(H�E�1�(�(��֑��H�{hL���
����sx��x���H�{h����H�E�dH+%(u	H��[A\]��թ��DUH��AVA��AUATA��SH��H��p��|���dH�%(H�E�1����H����H�{H�CI��H��t���H�;H�H��t���f����m�%km�I*��^�|���(�(�T�.�v,�,�f���5�lU��*�(����T��\�(�V�f���I*��^�|���(�T�.�v,�,�f���-�lU��*�(����T��\�(�V��,�L����x����,���|�������H�CH��H����H�{1��)���H�{� �����|�����x���H��H�I�����~	l(�H�]�H�u��H����x���Y���|����E�E��՗����x���H���2l��|�����k�^��
l�^�譞��H��L�������H�U�dH+%(u.H��p[A\A]A^]ÐH�
�;�1�1�H��;��1���蛧��ff.���UH��AWI��AVAUATSH���H��8���H�dH�%(H�E�1�H����
H��A�������A�G|A�΅�����R
A�����H��8����v���I�H��X���H��T���I��蜷��H��p���I�H��\���H��H��@����˭��M���2L��誹��H�E�L��H����(���H��H�������f��f����T����*M��=Gj�*U���4����\���X����\���T�����X���L��`���L��蓕����\�����p���L��軶����(���L��諟���sf��˛��A�W@A��A�G|��������!�Шu
A����L��`���I�L���϶��H��p����=�iH��@���H�E�H��H�����4���L��������iH��@���L��H��H�����p����E�賖��f���p����m��A*��/5�i��4���(�(�w-�4/uiv+�X�(�(�(��^��^�/Si�^�v	/Jiw��H,��H,ʼnʉ�A;G uA�G$9��<A�g@�(�I�(��(���������p�����(�������A�|�m����H,��H,�A�W A�G$I��A�������肗��H��I��H��蔐��M����H�E�dH+%(��L��H�e�[A\A]A^A_]�"���f�I�H��p���H��`������A��L������I�h�D���H�5�I��H���r���L��H��������p���H�u�L����`���H�E��E����M��tL��蟵��A�|thH�E�dH+%(�H�e�[A\A]A^A_]�fD�=PgH�E���X���H��H�����T�����4�����(��������I��������%���L��`����\����m��H,��H,ʼnʉ�A;G �#(�I�`��(����7�����p����m�����(�����A�|�$�����H,�A�G �H,�����DA.������������I�(�����A.���x����r���I�`�g���A�|uf�H,��H,�9�����4���I�H��(��������(��������H,E�A�G �H,�p����{����A�|���H,�I�H��\����A��H�����A�G �H,�A�G$H��X���H��H�� ���蒢��A��L���f���I�whI�`�ɞ��H�5�I��H������L��H��謍����\�����X���L��H��H���H�E��E�莪��A�G$f���A��f��H*�A�G �A*���^��^��H,�f���H*��^��H,����H�5_H��H��(����m���H��(���L������I�pHDžp������f�I�p���H*���x�������f�H��@���H��(������H*���|����ȩ��A�O@M��tL���f���A�G|�������6H�� ���H��T���L������H��@���H�����L��腑��I�wPI�H�X���H�5�H��I��薵��H��(���L���7���A�G f�L��f��H��H����A��H�E��H*�A�G$���
�c�H*��^�E����H��8�������H���D���H�5PI��H������L��L��跋��H��E1�1��,�p���L���,�X����,�T���P�D,�\���贤��XZM��tL���5���M��tL���(���A�G|����H��(����u���H�E�dH+%(��H��(�������H,��H,ԉ�A9G �����A�G$����A�G$9������F����A��I�w0uA�G@�_I�(�қ��H�5;3H��I������H��(���L��豊��A�G f�L��f���A��H��@���HDžp����H*�A�G$���
b�H*��^��x����l�����4���L��H���(�L���A^��(�薭��L��讚��H�5qI��H���l���L��L������I������L��I��H�����M��tL��訯��A�O@M��tL��薯��M���e���L��腯���X���I������蒐��H��I��H��褉��M���@���L���S����3�����p����m��W���H��脪��H�52H��I��貲��H��(���L���S���A�G f�L��f��H��H����A��H�E��H*�A�G$���
�`�H*��^�E�����M����������I�H�����m���E1�H�
�W�z1�H�5�
�y���L��
H�
�W�i1�H�5�
�X���蓜����UH��ATSH���G|HLJ��?��H�C0��H�=��*H�CPH��t"�=���H�5H�ChH��躝���Cx[A\]�f��K�H��0�H�501H�Q�*�|���H�=E�*I��H���j���L���r��H�=+�*�f���U1�H��ATSL�'�~ ���GI�D$ �x��tJ1��fDI�D$ H��;Xs2H�H�<��ƕ���p��t�A�|$,��ufA�T$��t=[A\]��A�L$��t�A�D$,��u�L��H�5��������A�D$,��A�D$H�5X�*L��[A\]�t���@A�D$,��A�T$��u������UH��AWAVAUATSH��H�}ȉu�M����H��H��t}H�5�M���d�����ujE�fM�nA�t	E���M��L�5�I��K�\I9�r�9�I��I9�s)I�}L��������u�I�}H��tH�5O�����tH��[A\A]A^A_]��H�E�H�@HH�XH��t,L�KM����H�x�u�A�p�H��A��H��H���+���L�}�H��H�
�*I��H�pH��L�8�˄��I�u@H��L��H���*����I� H��H��[A\A]A^A_]�[���E1�DM9��A���K�<H�=qH��H��H��M�t
I�6������xtL�{M9�r������I���M�����1��F���f���H�G �P��tbUH��AVA��AUI��ATS1��@I�E H��;Xs3H�L�$�L���<���D9�u�M��tI�} [L��A\A]A^]鮐��fD[A\A]A^]��ff.�U1�1�H��SH��H��H�8跅��H�C@H����H�PH���
���L�BM��t&H��*H�xH�sPH��A��H�C@H�PH���ް��H�J0H���Ѱ��1�H�x���H�PH�CHH��t!L�BM��tH�xH���H��H���*A��H�]����H�]�1�����UH��SH��H���+�����1���t�C(H�]���f.�H��H�5������\~���C(H�]���U1�H��SH��H��H� �I����{,����H���H��tH���H�H�BH���H��t	H�����H�{HH�CHH��t�z��H�CXH��tH�SPH�H�BH�CpH��tH�{P��H�{@H�C@H��t
H�]���{���H�]���f��C,�Ԯ���a���ff.�@��UH��SH��H������H�{ H�C H��t�u���H�{8H�C8H��t�ϩ��H�{0H�C0H��t�y����{(��u"�=���H���*H��H�]�H�@0����C(�4�����f�����u���t�f�UH��SH��H�������S��u�C(��t&H�]����CH�52�*H���R����C(��u�H��H�5����|���C(H�]���ff.���UH�G`1�H�@H��H�xH�@�P(��x�]���؉�����H��*�1�H��1��ς���]����UH��SH��H��H�`H�WH�BH��tH�@ H��t
H�z��H�{`H�]�����f���UH��AUI���ATSH��H�5��*���1�1�I�E �^���1��G���H�����hH�="�*I������H�5�H��H������I�T$L�c`H�BH����H�@H����H�z�Љƺ	H���Ў��H�C`H�PH�BH��tH�@H��tH�z��1�H�����H���n���I�]01�1�L���ޅ��I�E8H��tL���}�����tH��[A\A]]�fDH��H��)1�1�[� A\A]]�Y���f�������Y���fD��UH��SH��H��H�hH�ChH��t�Jw��H����>w��H���Hǃ�H��t�"w��H���Hǃ�H��t�w��H���Hǃ�H��t��v��H���Hǃ�H��t��v��H��H�,�*����H��HǃH��t�v��H���Hǃ�H��t�v��H���Hǃ�H��t�cv��H�{P�:v��H����.v��H����"v��H����f���H���*H��H�]�H�@0����UH��AWI��AVAUATSH��H�}�H�=��X���H�=�H���I���H��H�5�I��H�cHD�1�1�I���H������H��I���������L�}�M����F���1�H�5&H��1��À����I���H���?x��I���蓡��1�I���襞��H���]���H��H�H�=l'H��1��҆����H��H����w��H���J���H��I�����t��H�=��?w��I���M���������H�5�L������L�(H��M����H�E�A�E1�E1��bf.�N�t�I�H��t^1�H�5�L��A���^���I��H�E�H�8�_t��I�>�Wt��Mc�N�l��J��L�(H�E�I�GM��t#I��H�5:L���i�����t�A�����Mc�J��H��Ŏ��H��1�H�5H��1��Ot��L�u�H��I�Fh菏��H�@�*H�5Q�*H�="�*H���r���I����Ɗ��H��M��1�I��L����H�
X&�jH�5��赊��XZH�e�[A\A]A^A_]�@L���hs��H��H�=N1�����I�������������H��葅��foTH�foT@����f���UH��ATSL�e�H��L��H��dH�%(H�E�1�H�E��۝��H��tFH��H�5w�H�����H�5�*H��衏��H�E�dH+%(uYH��[A\]�f��u��H�}���苊����tL�������DH�E�H��$��1�H�H1��;|�������@��H���*H��tH���*��UH��SH���*H��H���ȉ����uH�m�*H�]����H�=�d���H�5]�*H���e���H��H���z���H�3�*H�]���ff.�f���H�
�*H��tH��*��UH��SH��*H��H���8�����uH�կ*H�]����H�=��ԋ��H�5m�*H���՝��H��H�����H���*H�]���ff.�f���H�u�*H��tH�i�*��UH��SH�T�*H��H��計����uH�=�*H�]����H�=�D���H�5}�*H���E���H��H���Z���H��*H�]���ff.�f���H�ݮ*H��tH�Ѯ*��UH��SH���*H��H��������uH���*H�]����H�=��贊��H�5��*H��赜��H��H���ʘ��H�k�*H�]���ff.�f���UH��SH��H�������
�*H��*����H���H��E1�A����H�C0H�
�@H����fHn�fHn�1�H�M�fl�H��H���H���H�5�CH�=�H���h�@�1���H�5���@��OH��f�H�=��H�@�*(�萑��H�9�*���H�5��H�=��E1�H��H��A��@�w��H��H�]��H��*XZH��*�����H�5��*H����r�����ff.����H��*H��tH���*��UH��SH��*H��H���H�����uH�ͬ*H�]����H�=�����H�5��*H�����H��H�����H���*H�]���ff.�f���H�m�*H��tH�a�*��UH��SH�L�*H��H��踅����uH�5�*H�]����H�=z��T���H�5
�*H���U���H��H���j���H���*H�]���ff.�f���H���*�@��UH��SH��H��Hc��*H�H�G ����H��H�]��H��靑��ff.�f���������UH�l�H��S��H���E�����H�]���ff.�f���UH��SH��H���+���H�,�*��*��ujH�S���H�\���H�߾H�C0H�����fHn�H�
]���fHn�H��fl�fHn�CfHn�fl���H�]���J���f.�H�5��*H���p���ff.�@��UH��SH��H���{���H�d�*�V�*����H�_���H�x���H��H�C0H�e��fHn�fHn�H����H�C@fl�C���H����H�
����fHn�fHn�H�T��fl�H�5ɽ��fHn���fHn�fl���H�]����H�5��*H���o���V���ff.����H���*H��tH���*��UH��ATSH���*H���u����uH��*H�e�[A\]�fDH�=�褅��H��E1�E1�j�0H�ƿH�
T����_n���PI��H�����L��H��贇��XH��*ZH�e�[A\]�f���UH��H��AWAVAUATI��H�=��*SH��H��dH�%(H�E�1�����H���7H��I��萐���I��H�@H��H�� ����Ք��I��H��0�������L��H��H��(���脘��L��L��E1�覈��H��@���H��I�_H��H��8�����m���/��TH���C���L��H���p��L��H��I���i��H��8����}��I��H��t$I�EJ� �@ ��u�H��L����s����I�} H��(���1��l��L��0���1�1҉�L���)r��H�� ���t�L��I���ܚ��H��H��I9�u�H��0����Ti��H�E�dH+%(uH�ĸ[A\A]A^A_]��΅��ff.�UH�=�H��SH��(dH�%(H�E�1��|���H��蔝��H����L�
T���jA�(H��H��H�
�����*l���H��H���ʇ��H�E��ܦ*H�����H�E�H�E�����H�U�H��H���En��H�E�dH+%(u	H��H�]����������Uf�H��AWAVAUATM��SH��H���H�ML����H�]H��dH�%(H�E�1�H�����HDž0���)� ���苅��H���~I��L�����A�}0�H�H��L�4H�����L���3���I��H������4���L��H��H��������H��L��L������H���ˆ��H��H�����H�����H�߻�܅��A�U0��H������YH��@���L��H������H��H�������j��H�����H�[L�$�H������H)�H���2f.��TL���C���L��L���m��L��I��H���f��H������z��I��H��t$I�EH��@ ��u�L��L����p����I�}(H�����H�� ����i���H��A���ɔ��1�H��D��H������o��H����{������H��1��˗��L�����M��t�L��H��I��託��I9�u�H������'f��H�E�dH+%(��H��[A\A]A^A_]�f�H�����贕��H��H�����H�X0H������H������~��H���Z���H�߻H���*���H������U���fD�f��L���L����H�
>H������ƺ1��y�������L���H�
�C�G1�H�5G�賔�����ff.�UH�=H��SH��(dH�%(H�E�1����H���4���H���0L�
d���jA�(H��H��H�
�����Jh���(H��H�����H�E���*H�����H�E�H�E��4���H�U�H��H���ej��H�E�dH+%(u	H��H�]����(������UH��AUATSH��H�����H��tBL�+I��M��tI9EtL��H��薇����t"L��L���m��H��H�@(H��[A\A]]��@H�H�5RC1���f��H��1�[A\A]]�ff.�����1�H�5�1�隃��f.���U1�H��AVM��AUI��ATI��SH��H�=���gr��AVM��H��AUH��A�����1�H�5���6���XZH�e�[A\A]A^]����UH��SH��莓��H��H��tH��H�5L�1�����H���Mc��1�H��H�]�����ff.�@��U1�H��AUI��ATI��SH��H�=��H���q��H��1�H��AUH��M��A�����H�5���{��ZYH��H��tH��H�5��1��:��H����b��1�H����H�e�[A\A]]�ff.�@��U1�H��AUM��ATM��SH��H�=t�H���(q���uM��H��AUH��A�����1�H�5[����XZH�e�[A\A]]�f���UH��SH���N���H��H��tH��H�5�1��~��H���
b��1�H��H�]�����ff.�@��U1�H��AUM��ATM��SH��H�=��H���xp��H��1�H��AUH��M��A�����H�5���z��ZYH��H��tH��H�5��1��}��H���a��1�H����H�e�[A\A]]�ff.�@��U1�H��AUM��ATM��SH��H�=F�H����o���uM��H��AUH��A�����1�H�5.�趓��XZH�e�[A\A]]�f���UH��ATI��H��SH������H��H��tH��L��H�5��1��;}��H����`��1�H��[A\��]�fD��U1�H��AUM��ATM��SH��H�=��H���8o��H���u1�H��H��M��A�����H�5|��ty��ZYH��H��tH��L��H�5l�1��|��H���>`��1�H����H�e�[A\A]]�ff.���UH�=�1�H��SH��H���n��H��H�]��H���ҁ��f���UH�=��1�H��SH��H���n��H��H�]��H��颁��f���UH�=��1�H��SH���H���Pn��H��H�]��H���p�����H���*H��tH���*��UH��SH�|�*H��H���i����uH�e�*H�]��������H��H����{��H�A�*H�]�����UH��AWI��AVI��AUA��ATM��SL��H��H�M��c���jH��1�H��H���L�
��PH���PH�M�H���QL��PH���AWPH��AVP1�AUL�E�1���H��PH�e�[A\A]A^A_]�ff.���UH��AUI��ATI��SH���s��L��L��H��H����u��H��I���X^��H��L��[A\A]]�f.���UH��AWI��AVI��AUA��ATM��SL��H��H�M��s���jE��L��H��H���H��PH�	�L�
)PH�M�H��QH�
��PH��AWP1�AV�x��H�e�[A\A]A^A_]����UH��AWA��AVI��AUA��ATM��SL��H��H�M����jH��1�H��H�E�L�
�PH�v�PH�M�H�{�QL��PH���AWPH�{AVP1�AUL�E���H��PH�e�[A\A]A^A_]�ff.���UH��AUI��ATI��SH���4r��L��L��H��H���Ct��H��I����\��H��L��[A\A]]�f.���UH��AWA��AVI��AUA��ATM��SL��H��H�M���jE��L��H��H�T�H��PH���L�
�PH�M�H���QH�
�PH���AWP1�AV�v��H�e�[A\A]A^A_]����H��*H��tH�	�*��UH��SH��*H��H���(f����uH�ݙ*H�]�����{���H��H���x��H���*H�]�����UH�����1�]H��1���[��fD��H�up*�@��UH��SH��H��HcQ�*H�H�G ����H��H�]��H���}��ff.�f���������UH���H��AUATI��S��D�kH���r���sL��H����r��D��L��H����q��H��D��[A\A]]����UH��SH��H���ۆ��H���*���*��ujH�3���H�����H�߾H�C0H�i���fHn�H�
]���fHn�H��fl�fHn�CfHn�fl���H�]������f.�H�5!�*H���1^���ff.�@��UH��SH��H���+���H�ܗ*�Η*����H�����H�����H��H�C0H�����fHn�fHn�H�D���H�C@fl�C���H����H�
e���fHn�fHn�H�����fl�H�5Y���fHn���fHn�fl���H�]����H�5)�*H���Q]���V���ff.����H�-�*H��tH�!�*��UH��ATSH�
�*H���Jc����uH���*H�e�[A\]�fDH�=�Ts��H��E1�E1�j�(H�ƿH�
���\���PI��H���_m��L��H���du��XH���*ZH�e�[A\]�f���UH��H��AWAVAUATI��H�=cm*SH��H��dH�%(H�E�1��w��H���7H��I���@~���I��H�@H��H�� ���腂��I��H��0�������L��H��H��(����4���L��L��E1��Vv��H��@���H��I�_H��H��8����y[���/��TH����L��H���X^��L��H��I���W��H��8����j��I��H��t$I�EJ� �@ ��u�H��L���a����I�} H��(���1��nZ��L��0���1�1҉�L����_��H�� ���t�L��I��茈��H��H��I9�u�H��0����W��H�E�dH+%(uH�ĸ[A\A]A^A_]��~s��ff.�UH�=
H��SH��(dH�%(H�E�1��,q��H���D���H����L�
$���jA�(H��H��H�
������Y���H��H���zu��H�E��T�*H�����H�E�H�E�����H�U�H��H���[��H�E�dH+%(u	H��H�]����r�����Uf�H��AWAVAUATM��SH��H���H�ML����H�]H��dH�%(H�E�1�H�����HDž0���)� ����;s��H���~I��L���{��A�}0�H�H��L�4H�����L������I��H������4���L��H��H�����蒃��H��L��L���s��H���{t��H��H���p���H�����H�߻�s��A�U0��H������YH��@���L��H������H��H������X��H�����H�[L�$�H������H)�H���2f.��TL����L��L���X[��L��I��H���T��H������g��I��H��t$I�EH��@ ��u�L��L���^����I�}(H�����H�� ����iW���H��A���y���1�H��D��H�������\��H���i������H��1��{���L�����M��t�L��H��I���X���I9�u�H�������S��H�E�dH+%(��H��[A\A]A^A_]�f�H������d���H��H�����H�X0H���΁��H������bl��H���
y��H�߻H����q��H������U���fD�3T��L���L����H�
�H������ƺ1��)w������L�p�H�
�.��1�H�5��c����o��ff.�UH�=XH��SH��(dH�%(H�E�1��Lm��H�����H���0L�
t���jA�(H��H��H�
�����U���(H��H���q��H�E��\�*H�����H�E�H�E��4���H�U�H��H���X��H�E�dH+%(u	H��H�]�����n�����UH��AUATSH��H�����H��tBL�+I��M��tI9EtL��H���Fu����t"L��L���[��H��H�@H��[A\A]]��@H�aH�5�.1��T��H��1�[A\A]]�ff.�����1�H�50�1��Jq��f.���UH��AUATSH��H���7���H��tBL�+I��M��tI9EtL��H���t����t"L��L���Z��H��H�@ H��[A\A]]��@H��H�5�-1���S��H��1�[A\A]]�ff.�����1�H�5��1��p��f.���UH��AUATSH��H�����H��tBL�+I��M��tI9EtL��H����s����t"L��L���GZ��H��H�@H��[A\A]]��@H�H�5-1��+S��H��1�[A\A]]�ff.���U1�H�5��H��H��dH�%(H�E�1�H�U��p��H�E�H�U�dH+%(u���l��f���H��1�H�5��1��o��f���H���*H��tH���*��UH��SH���*H��H����Y����uH�}�*H�]�������H��H���@l��H�Y�*H�]�����UH��AWI��AVI��AUA��ATM��SL��H��H�M��c���jH��1�H��H�/�L�
��PH�F�PH�M�H�K�QL��PH�M�AWPH�K�AVP1�AUL�E�q��H��PH�e�[A\A]A^A_]�ff.���UH��AUI��ATI��SH���d��L��L��H��H���f��H��I���N��H��L��[A\A]]�f.���UH��AWI��AVI��AUA��ATM��SL��H��H�M��s���jE��L��H��H�>�H��PH�Y�L�
y�PH�M�H�W�QH�
��PH�U�AWP1�AV�Wh��H�e�[A\A]A^A_]����UH��AWA��AVI��AUA��ATM��SL��H��H�M����jH��1�H��H���L�
j�PH���PH�M�H���QL��PH���AWPH��AVP1�AUL�E�p��H��PH�e�[A\A]A^A_]�ff.���U1�H�����1�H��SH��H���W��H��1��SL��H��L�
jH�
y�����XH�]�Z��fD��UH��AUI��ATI��SH���$b��L��L��H��H���3d��H��I����L��H��L��[A\A]]�f.���UH��H��AWAVAUATL�e�SL��H��H��(dH�%(H�E�1�H�E��r���H����H��H�3��1�1��V��H���Qg��I��H����L�-��H��L��������SH��L���n��L��H�=��H��1��Z��H��I���^��H��H�E��Z��H��I���&d��SH�U�M��H��H����L�6�L��PH�
��AVj�jj��u��H��0L���g��H�E�dH+%(�H�e�[A\A]A^A_]��H��L����m��H���H�=��H��1���Y��H��I����]��H��I���Y��H��I���hc��SM��L���H��H����L��L��PH�
�AWj�jj�u��H��0�F����H�5�*H���g��L���Af���#���@�M��H�}�����b����tL���p�����f�H�E�H����1�H�H1��3T������f��ff.����UH��AWA��AVI��AUA��ATM��SL��H��H�M��3���jE��L��H��H���H��PH��L�
9�PH�M�H��QH�
��PH�"�AWP1�AV�d��H�e�[A\A]A^A_]����H�m�*H��tH�a�*��UH��SH�L�*H��H���S����uH�5�*H�]�����[���H��H���f��H��*H�]�����UH�����1�]H��1��vI��fD��H�͆*H��tH���*��UH��SH���*H��H���(S����uH���*H�]�����۫��H��H���e��H�q�*H�]�����H���*H��tH��*��UH��SH�܅*H��H���R����uH�Ņ*H�]����蛉��H��H���e��H���*H�]�����UH��SH��H���Kt��H�ԅ*�ʅ*����H��
H�
�fHn�fHn�H��~��fl�H�
BfHn�CfHn�fl�C(�!���H��E1�E1�jH��1ɺjH�=��1�j�9Z��H�� �G�*����A��E1�H�'�H��H�5��H�="���O��A��1�H��H�5:�H�=�H�Ȅ*�+K��A��1�H�T�H�5��H�=��H���*��M��H���*��h��A��H�I�H�5��H��H�=s��#y��H���*��k��A��H�:�H�5��H��H�=����x��H�M�*�8w��H�I�H�5��A��H�=��H����x��H��H�]�H���*H� �*���Y��DH�5�*H���aJ���F���ff.����UH��SH��H�����H��t0H��H�H��tH;0tH���^i����tH�5��*H��H�]����b��L��H�
���1�H�5���5u��D��UH��ATI��SH��H��dH�%(H�E�1�H�E����H����H��H�H��tH;0tH����h������L��H�u���I��H��t3H�C0E1�E1�L��H��H����H�5t�L�`8��\��L������uoH�{0t�SH��H�}����#]����t/H���'E��H�}�H��t�IJ��H�E�dH+%(u>H��[A\]ÐH�C0H�x@H�@@H��t���D���@H�5I�*H���a���}����Ga��L���H�
���1�H�5g���s��fD��H�G H��uH�G8�fDH���0j��UH��AUATI��H��SH������I�|$PH��I���]��H��H��tH��H��[A\A]]�f����p��L��H���T��I�|$PH��H����T��H��H��[A\A]]�ff.�f�UH��AUATI��SH��H��HdH�%(H�E�1��Y���H�XA+\$@��v9��f�Hi�%I�$H�� )������*��Xf/�#�wkA�D$4��t"H�E�dH+%(��H��H[A\A]]�@�,L��H�5�?�e��A�D$4��H�E�dH+%(umH��HH�5�[A\A]]�`m��I�t$PH�]�L�m�H���V���f�H�E��L#�Y�1�L��H����\����u�A�D$4���L����i����T_��@��UH��AUATI��SH��H��L�jHM��t9�v��H��L��H��H��4�ׂ�CH��H��?H��H)����H�{HH��t�wB��H�SH1�L��1�H�5���b���u��H��H��4�ׂ�CH��H��?H��H)�H�S@H��[A\A]]Ð��UH��H�5>�H��ATI��SL��H��dH�%(H�E�1��g����t!H�E�dH+%(�H��[A\]�DH�U�H�5��L���%^���E�1҃���;S8t��S8��vH�sHH��t�H�C@H��H�P����f���t��H��H��4�ׂ�CH��H��?H��H)�H�S@�n�����]��f���UH��AUI��ATSH��H�����H�{PH���hZ��L��I�����H�{PH���QZ����A\$H��[A\A]]�,�����UH��SH��H���+���H��t^H��H�H��tH;0tH���c����tBH�{ H��uH�C(H��tH�]���f�H�]���o��fDH�=����^��H�C(��fDH�~�H�5�1��#C��1��ff.�@��H�G H��tH����K��UH�G0H��H��t#H�@H��t3H�8H��t�`��H��t]�fD�H�5T�H�=��]�c���zy��f.���H� H��t�r��fD1��D��1�H� ��Ð��UH��AUA��ATI��SH���F��D��H��H����\��H�5��H����n��L��A�H�
��H��H�5����h��L�������uH��H��[A\A]]�fDH��H�5���g��H��H��[A\A]]�ff.���UH��AVAUATI��H��SH��H���Z��H��tZL�5c�H��I��L���r��H��H��tMI�D$0H�x H��t}H�5@��g��H�5<|*H��L��[A\A]A^]�x[���H��[A\A]A^]�H���U��L��H��I�D$0H�x0��b��H�
�x*L��H��H��H�E��A��H�U��u���L���H�
D��1�H�5���qm������G��t�������UH�5��H��AUATSH��H��L�g0I�|$ �3N������H�[ H��t~L�-��H��L����R������L�-w�H��L����R����urH���m��H��tH�58�H���&q����uBI�D$I�|$(H�t2H����S��H��t%H���mJ��H������H��[A\A]]�f�H���[A\A]]�L��H���eK����������ff.����G����UH��H��ATI��SH���'�����t[A\]�fDL��H���]���H��I�������u��(p��H��H��4�ׂ�CH��H��?H��H)�I�T$�fD��H�G0H��tH�x��G��f.�1��ff.�f���UH��ATI��SH���:�����tEH�C0H��t,H�XH��u�!�H�[H��tH�;�?D��I9�u�[�A\]�[1�A\]�f��{�t�L����Y��9C[��A\]���@��UH��SH��H����H��H�]�1��H��H�5��1��<����UH��AUATSH��H��L�o L9�t!I��H��tH���C;��L�c M��tL���;��H�{@H�C@H��t�L;��H�{ tH�����H�����H���H��H�C@H��[A\A]]�ff.�@��UH��AUI��ATSH����tgH�I�̉�H�8�GE��I��I�$H�8�8E��AUL�
.�1�PA��1�H�
��A�t$H���S�D��H�� H�e�[A\A]]��H���K��H�e�L��[H��A\A]]������H�G01�H��t!H�x8H��tUH������1�]���‰��@���ff.�f���UH��AUATI��H������I�Չ�H�PHc�H�>��fDH�w H����H�e�L��A\A]]�JX��f.��wH�e�L��A\A]]�Y��@�C���H�e�L��A\��A]]��N������H�e�L��A\H��A]]��N���H�G0H��tjH�p 놐���H���x���H��u�H�8�C��I��I�$H�8�C��AUL�
��1�PA��H�
	�1�A�t$�u�H���V��C��H�� H�e�A\A]]���UH��AUI��ATI��SH��H���A�M����H��I�$H��tH;0tL���\������Bb��H��H��tJH�H��tH90tH����[����t1H�5jH���j����tfL��H���i������H��[��A\A]]�H�D�H�5�1��;��1�H��[A\A]]�fDH�N�H�5j1���:��1����H��H�5J1���:��1��ff.�@��H�G ����UH��AVAUI��ATSH��H���sN��I��H�C0L��H�x(�l����u[A\A]A^]�L���N��I��H��t�M��t�L�s0I�~(I�F(H��t	�q7��L�s0L���uG��L��L��I�F(H�C0H�x0��[��H�5��I��H�C0L��H�x �_��[L��A\A]A^]�D7��@��H�v@H�@�O\��ff.�@��H�=t*H��tH�1t*��UH��SH�t*H��H���A����uH�t*H�]�������H��H���pS��H��s*H�]�����UH��SH��H���b��H��s*��s*����H�/���H�C0��H���N���SE1�E1�jH��1ɺjH�=0�1�j��H��H�� �os*����H��E1�E1�jH��1ɺjH�=�1�j�H��H�]�H�� �8s*��f�H�51s*H����9���f���ff.����UH��ATI��H��SH�������t&��t=��tYE1�H�
o
��1�H�5���L^���5�r*H��L��[1�A\1�]�cE��H���(5��1�H��I�D$H�8�F����@I�D$H��H�8�[���ff.���UH��AVAUL�u�L�m�ATL�e�S1�H��@dH�%(H�E�H�GL��H�0��G����H�u�H���4;��H��L��L��L����N����u�H�5��H����@��H�U�dH+%(u
H��@[A\A]A^]��/Q��ff.�@��UH��AUATSH����C��H�I��I��H��u(�<f.�1�H������g����uH��H��H�;H��u�I�\$I��H��u�H��L��[A\A]]���H�5q*H��tH�)q*��UH��SH�q*H��H���(>����uH��p*H�]�����Ku��H��H���P��H��p*H�]�����UH��AUI��ATI��SH��H�P��L��I�|$PL��H���L��H��1�H	�tH��t!H��t.��\�,�H����[A\A]]�H���[��A\A]]ú���������H�p*H��tH��o*��UH��SH��o*H��H���8=����uH��o*H�]�����;���H��H���O��H��o*H�]�����UH�����1�]H��1��2��fD��UH��SH��H���[���H��t.H��H�H��tH;0tH���U����t���H�]���fDH�}�H�5�	1��5���������ff.����UH��ATA��SH�����H��tUH��H�H��tH;0tH���=U����t9D9��tP�c@�H�{D���tH����=��H�5�n*H��[A\]�N��[H���A\1�H�5
	]�g4���[A\]�ff.���UH��SH��H���K���H��t.H��H�H��tH;0tH���T����t���H�]���@H�m�H�5j1���3��H�]�f���f���UH��AVfA~�SH������H��tcH��H�H��tH;0tH���+T����tGfAn�.��ztW�c@�H�{D���tH����<��H�5�m*H��[A^]�M��f.�[H���A^1�H�5}]�G3���[A^]�ff.���UH��SH��H���+���H��t&H��H�H��tH;0tH���~S����t
�C|H�]��ÐH�U�H�5�1���2��H�]����������UH��ATA��SH������H����H��H�H��tH;0tH���S����tuD9c|���c@�D�c|E��t&H�{tH���;��H�5�l*H��[A\]�|L��@H�{XH�CXH��t�/��H�{HH�CHH��t��|/��H�{u��[H���A\1�H�5�]�2���[A\]�ff.���UH��AUI��H��ATSH����tL��t%��uX�4��H�e�L��[��A\A]]������1��H�e�L��[��A\A]]������C��H�e�L��[A\A]]����I�EI��H�8��8��I��I�$H�8��8��AUL�
ع1�PA��1�H�
�A�t$H����S�@8��H�� H�e�[A\A]]Ð��H��j*H��tH��j*��UH��SH��j*H��H���X8����uH��j*H�]�����o��H��H���J��H��j*H�]�����H�]j*H��tH�Qj*��UH��SH�<j*H��H����7����uH�%j*H�]�����o��H��H���@J��H�j*H�]�����UH��SH��H���H��H���H��P���H��X���L��`���L��h�����t#)�p���)M�)U�)]�)e�)m�)u�)}�dH�%(H��8���1�H�=yi*t7H�8�H�5i1��/��H��8���dH+%(u]H�]���f.�Dž ���H�EH��(���H��@���Dž$���0H��0������H�� ���H��H���S��H��h*��H��ff.���H��h*H��t��UH���H�5�1�H��H��H�E���.��H�E��Ð��H�GhH�GhH��tH���,���ff.�@��UH��AVAUATI��SH�����M����H��I�$H��tH;0tL����N�������WI����t[A\A]A^]�f.�H���6���Hc�I���h,��I��H��t+H�H�[H���
f�H��Jf�H��J�Rf�H�f�P�H��u�I�t$`H��ucI�|$HL��D���[L��L��I�D$`�+��I�|$0��V��I�t$`[H��A\A]A^]�/K���[H�V�A\1�A]H�5A^]�-��I�|$H��^���ff.����UH��SH��H���@��H���Y��H�C�:���H��H�C H�]�H����v�fD��UH��SH��H��dH�%(H�E�1����H�U�1�H�5μH��1��J��H�}��L*��H�}�H�E�dH+%(u
H��H�]����C�F�����UH��SH��H��dH�%(H�E�1����H�U�1�H�5^�H��1��5J��H�}���)��H�}�H�E�dH+%(u
H��H�]����B�7F�����UH��SH��H��dH�%(H�E�1�����H�U�1�H�5�H��1���I��H�}��l)��H�}�H�E�dH+%(u
H��H�]���B��E�����UH��SH��H��dH�%(H�E�1����H�U�1�H�5~�H��1��UI��H�}��(��H�}�H�E�dH+%(u
H��H�]���<B�WE�����UH��AVM��AUI��ATA��SH��H��dH�%(H�E�1��-���H�U�1�H�5��H��1���H��H�}��}(��H�}�H�E�dH+%(uH��M��L��D��H��[A\A]A^]��A��D�����UH��SH��H��dH�%(H�E�1����H�U�1�H�5~�H��1��UH��H�}��'��H�}�H�E�dH+%(u
H��H�]���A�WD�����UH��AUA��ATA��S��H��dH�%(H�E�1��3���H�U�1�H�5�H��1���G��H�}��'��H�}�H�E�dH+%(uH��D��D���[A\A]]�>��C��@��UH��SH��H��dH�%(H�E�1����H�U�1�H�5��H��1��eG��H�}��'��H�}�H�E�dH+%(u
H��H�]���<?�gC�����UH��H��dH�%(H�E�1��P���H�U�1�H�5"�H��1��F��H�}��&��H�}�H�E�dH+%(u��>�C��f���UH��AUA��ATI��SH��H��dH�%(H�E�1����H�U�1�H�5��H��1��F��H�}��2&��H�}�H�E�dH+%(uH��D��L��H��[A\A]]�w>�B��f���UH��H��dH�%(H�E�1��p���H�U�1�H�5B�H��1��F��H�}��%��H�}�H�E�dH+%(u��7>�"B��f���UH��AVE��AUA��ATA��SH��H��dH�%(H�E�1����H�U�1�H�5ϷH��1��E��H�}��M%��H�}�H�E�dH+%(uH��E��D��D��H��[A\A]A^]��=�A�����UH��AUI��ATA��SH��H��dH�%(H�E�1��r���H�U�1�H�5D�H��1��E��H�}���$��H�}�H�E�dH+%(uH��L��D��H��[A\A]]�G=�A��f���UH��SH��H��dH�%(H�E�1����H�U�1�H�5ζH��1��D��H�}��L$��H�}�H�E�dH+%(u
H��H�]���>�@�����UH��H��dH�%(H�E�1����H�U�1�H�5b�H��1��9D��H�}���#��H�}�H�E�dH+%(u��>�B@��f���UH��SH��H��dH�%(H�E�1��,���H�U�1�H�5��H��1���C��H�}��|#��H�}�H�E�dH+%(u
H��H�]���=��?�����UH��SH��H��dH�%(H�E�1����H�U�1�H�5��H��1��eC��H�}��#��H�}�H�E�dH+%(u
H��H�]���=�g?�����H� �ӑ��UH��ATSH�� dH�%(H�E�H�G0H�����@uaH�XE1�H��u�/H�[H��t#H�;�o;����u�H�3L���(��H�[I��H��u�H�E�dH+%(�~H�� L��[A\]��0���H��H�}�����H����H���TK��H���S��L�c0H�U�H�5M���H�E�I�|$�/��I�D$H�C0�`��L���H�E�dH+%(uH�� 1�[A\]��N>��ff.���UH��AWAVAUI��ATA��SH��H��XdH�%(H�E�1��n���t)H�E�dH+%(�iH��X[A\A]A^A_]��L�����I��H����H��H���a,��H���U���H����I��H�E���H���H���?J��H���R��H��H�E��)��L��H�E��@��D��L�����1�����*L���!��H���Z/��I��H��t$I���I�?H9�t	H�u��B��M�M��u�L���6U��H��H�E��(��H�U�H�5k���H��H�E�H�E�H�E��'5��H�u�H�}��::��H����.��H�}�I��H�E���T��H�E�M����M�/L����=����������M�M��u�H�}��T��I��H�}�H�E�H9���D��L���5��M���Q���H�E�dH+%(��H��XL��[A\A]A^A_]�QT���H���!����fDH��tkH�H��H���*��H���L����f�H�}��T��M���l���L�����H�߉E��{���U�H�}����0�����F����>���D��L���5G���M���1�1��G*��H��������1��S�������;�����UH��AVAUATS����H��tgI��I��E1�fDI�>� ����~Hc�L��H����)��H��t!M�vM��u�L���HS��[L��A\A]A^]�@L��H����$��I�����E1�[L��A\A]A^]Ð��UH��AUATSH��H���7���1���tH��[A\A]]�H�C0L�-��L��L�` L���f.����uRH�C0H�XH��t4DL�#L���5'����t��H����L����:3��H�[H��u�H���[A\A]]ÐL��L���0��H��u�1�L��L���s=���А��UH���c�1�]H���xR�����UH��AVA��AUI��ATI��S����D���H���$�I�} L��H��H����/��H��[A\A]A^]�B��f���UH��AWAVM��AUI��ATI��SH��H��H�E��8���D��<���H��0���H�EH��(���dH�%(H�E�1��%��H����H��H�H��tH;0tH����?�������'M��L�{ H��M���7I�H��tH90tL����?�����L���2B��H���I�����bM���yA�<$�nM��tH�=ٮ� ��L��H���RJ��������<�����uM��t%�E��H��I�H��tH90tL���;?������H��(���H��0���L��H���ZK��H�5����I��H��H��0����#��L���*��H���pM���GH��@���H�5G�H��H��(�����1��H�{ ������<�����8���H���6�I��H����H�s 1�H���L8��H��H���@H���X!��H��(���H��H�5ҧH��1���K��H���A,��H��(���H�ڭH�5��H��1�H���K��H��� ��H��L������L��L��H��1�H�=���u)��H���=1��H�U*H��0���H���:��H�E�dH+%(��H��0���H�ĸL��H����[�A\A]A^A_]�C���H�E�dH+%(�aH�~�H�ĸH�5��1�[A\A]A^A_]�����H�E�dH+%(�!H���@H�E�dH+%(�H�F��@H�E�dH+%(��H�n��{����H��(�������H������@H�E�dH+%(��H�(��C���f�H�E�dH+%(��H�>������1�H�=�1��(��I������H��0���H�5ܫ�%���x����H�E�dH+%(u%H�2����DH��(������H�������w5�����UH��AWM��AVA��AUA��ATI��S��H��8�X�I�|$ H�E��`�C�D��D��H��H�E��a�I�Ņ�������L������1ҾH���'��I�|$ AWL��P��L�M�L�����PH�&����1�j�jP�U9��H��0A�ą�yL���2��H�e�D��[A\A]A^A_]É��	L�����I�|$ H�5��/&�����j���H�}��ނH��H��������H�E�H����H�}��=�������E1�H�E�fDH�}�L�����H��H���H�=������H��H���E������1�H�5p�H���X(��H�E�H����H���"������H��1�H�5G��*(��H�E�H��H����1��?��H�0H��t3E1��A�VL��H�E�H���L/��H�E�A�VI��H�4�H��u�H�����H�}�����H�}����H������4����1����H�����H�E�I��I9���H�y���1�1��[ �����fD1��Y�����1��I��H�}��P���fDI�D$0H��tH�@H��t_H�8H��tD���5>��A����f.�H����1������{����H�I��1�1�����Y����aO�����UH��AWAVAUA��ATI��S��H��dH�%(H�E�1��?�I�ƅ���A�D$��t/��tU����L�}�1�D���M��L��H�E�������tkH�E�dH+%(��H��[A\A]A^A_]�f�H�E�dH+%(��H����L��1�[A\A]A^A_]�S�H�������]����L�����H�5�H�=��H���A��H��H��1��u&��L��H��H�E�H��H�P�~H�����L���?:���?���E1�H�
p��1�H�5N���C���1�����1Ҿ���������UH��AVAUATSH��H� H����A��L�-��"D��L��H����G������H�C0H��tL�h M��tL�5��L��L���K$����uD��H��E1�[1�A\1�A]A^]���DL��L���&��H��u�[L��A\L��1�A]A^]�3��[H��A\1�A]H�5[�A^]�S��D��L��H��1�[A\A]A^]������UH��H��AUATSH��H�������1���tH��[A\A]]ÐH�����I����L��H���%gI�ĸM��t�H�����H��H���%B��L��A���B��A9�tH���[A\A]]�DL���x3��H��I���m3��L��H����7����u�L���F��H��I���;��L��H���pG����u�L���D��H��I���	D��L��H���NG����u�L�����H��I�����L��H���7�����_���L���,E��H��I���!E��L��H���G�����9���L����/��H��I���/��L��H���@7��������L���p@��H��I���e@��L��H���J/�����������ff.�f���UH��AUATI��SH��H���$��I���\�L��H���eH��tH��[A\A]]ÐH�����H��H��t�H�3L���>��H�[H��u�H��[A\A]]Ð��UH��AUATI��SH��L�oI�}�*��H��H��tH��H��[A\A]]�f��ˍL��H���eH��H��t�H��� ��H��H���e��I�}H��H���!��H��H��[A\A]]����UH��AWAVAUI��ATI��SH���P���H��H��tH��H��[A\A]A^A_]�fDL�=y$*H�ң�
I�_I��H��t�1�H��L��1��D6��L��H��I�����L��H���;��H��t��@��UH��AWAVAUATSH��H����I��L�5�1�I��L��L��1���5��L��H��I���(���L��H������H��tH��H��[A\A]A^A_]�fDL��H������-���-H�5̮I��H�����1�L��L��1��{5��L��H��I�����L��H���r��L���j����1��ff.����H��tGUH��SH��H��H�GH�x�~(��H��H��tH��H�]������f�H�]�1����1��ff.�f���UH��AWAVAUATSH��H��(H�u���.��L�{H�E�A�G A�G$��uH��H�5�y����	����A�G L�{I��b���M�H���5bI���.��H�E�M�����I�H���'��H�E�H��t;H��L���$������H���h��I�|�?tH�55��b3����u~fDL��� ��H��I�����I�L��H������M�d$M��t|M�4$L���3��L��I���U��H��H��t�L���<��A�ƅ��R���H�}�L���:���A���f�E��t�H��G*H�u�1�H�}��������e���M�d$M��u�H�}�t	H�}��9��H�]�1�H�5�H�CH�x����H�CL�}�H�5w���H�8L���l=��1�L��H�5�U����4��L����@���5\J*H��1�H��(1�1�[A\A]A^A_]����ff.����U1�1�H��AVAUL�-1G*ATLc%J*SL��H��I�L�g1��[��L�5�F*L��1�L�-�F*I�$L��L���9��H��F*L��L��I�D$H�����I�D$�%�H��E1�E1�I��H�5l�H��H�X�����#��H��L��[A\A]A^]�@�����UH����]H����_f.���UH�����]H�����f.�H�G0H��tgUH��SH��H���W��tK�@��t4��t?�G���H��H������H�51I*H��H�]���(��@��t�G��H�]���f�H�l�H�5��1��;��ff.���UH��SH��H�z0t9H��������uH�C0H��H�]��@��;���H�C0H��H�]��h��#���L���H�
E���1�H�5ӕ�R:��f���UH��AVAUATI��SH�G0H��H��t"H�x�	��H��tH�e�[A\A]A^]��H���!��H�C0H���c�HL���_
��L�k0H��I�}����E1�H��L��I�EH��q��H�5���>��E1�H��L��H���H�5��>��L��H���X��L�k0I�}8t]L�������uH�C0�@H���!����C�����C����H������5�G*H��1�H�e�1�[A\A]A^]����f�I�}@u�I�}(t�I�}1��G8��H���/ ��I��H���v���� ��H��I�E@�b	��H��I�U(L��M�E@I�}0P�L�
����]���ZY�9���fDL���%�����J����s1�L����,���8���fD����H���+qH���s2��H�{0I��um�H��5��E1�E1�H��H�C0L��I��H��p���H�5O��s ��1�1��I�EL�k0�
��I�E0L�k0I�}0t3�95��I�E H�C0����L��H�
n��z1�H�5l���7��L�ԿH�
M���1�H�5K���7��f.���UH��ATI��S譿��1�H��1����L��H�����H�=���1�����L��H��H�C8�'���H��[A\]�ff.�@��UH��AUATSH��H�G0H����H��H�xI�����H����L�k0L��I�}�:&��L��I�E�>������H�����L�k0I�}��H��L��L�
�n��1�SE1�1Ҿ���H�$1ҾL��L�
����E1�1����L���h��X�5�D*H��ZH�e�1�1�[A\A]]�*��f.�H�C0�h�m���H�e�[A\A]]�DH�C0���H����nH���30��A�U������A�U�;���I�uH����I�}8I�E8H��t����I�}@H��t���I�}@I�E@H��t���I�} I�E H��t���I�}0I�E0H��t�w��I�}(I�E(H��t�A��L���9�����@I�EH������^���L��H�
���Q1�H�5ې�Z5��L���H�
|���1�H�5���95��f���UH��ATSH��H�(H�C(H��t �����f.�H�@H��H�0���H�C0H��u�CH�{ ��tgH��tt�#��H�{ I���&��M��L�
]�I��H�(�H���LD�M��H�"�LD�H�*��{LD�L��1�1��
��H�{ H�C H��t�$��H�{0uH��B*H��[A\]H�@(��L�\�H�
����1�H�5���(4�����UH��AUATI��SH��H��H�������t]H�C0�SH��tAH�xt:��t\�C�i���H��H�����H�5�A*H��H��[A\A]]�E!��D��t#�C��H���8����tL���,���CH��[A\A]]�fD����H���lI�ŋC��t��u+H��H�5}�1����L���,��L�������f��C���H��H�����H�5:A*H��� �������UE1�1�1�1�H��AWAVAUI��ATL��X���SM��H��dH�%(H�E�1��G4H�HDžX����(
��H���H��H���$.��H��I���I��L���Q)��L��H���6��1�L��H��H�5�������unH��X�����H�����H��X���H��t%H�H1�H�?�1������H��X������H�E�dH+%(��H�Ę1�[A\A]A^A_]�1�L��H�5��H��������v���I�uPL��p���L���q��H��`���H��@���H��h���H��H���DH��@���H��H���L���J�����"L��h�������H��L����H��t�1�L��H�5�H����������H��h���L��H�5��H���am��������H��`���L�u��'L����%��L��L��H��H�5���(m���������H��`���H�=���p1��8��H��L��H�5ӎH��I����l��L��A���U��E���N���1�L��H�5t�H�����������-���H��X����@���@E1�1�1�1�H���w������f�1�L��H�5,�H����������L��1�H�5�H���p�������f��fD��UH��AWI��AVL�u�AUATL�e�S1�H��HdH�%(H�E�1��z�I�wPL��I�����1�L��L�������t/H�u�L����H��H��t����H��H�����H����fDL��H�5��H�����H�U�dH+%(uH��H[A\A]A^A_]������UH��AUATSH��H��dH�%(H�E�1����H�!:*H�5�9*H�=:*I��H���h��H�CP�_tE1�E1�H��H��H���H�5�� ���{�E1�E1�H��H��H����H�5o����1�1�����jL�
Ó1�jL��1�L��H��I���Z"��E1�E1�H��H�C H����H�5��H�����L���/���1�1�H�U�H�C8H�5��L���`��H�}�1�1�H�5}��<
��H�}�I������L���+��L��H�C���H����l��H�=[����H��E1�E1�H�C(H��H��H�5R����H�{(H�5K����Y^A�ą�t,�S<��t]D�c<H�E�dH+%(u^H�e�[A\A]]���C<��t�H�{HH��t�K����{4H�CH��t��C4�2���fD�rH��1�H���F�������ff.�@��UH�5��H��ATSH�z(H���2���A�ą�t�S<��tDD�c<[A\]��C<��t�H�{HH��t����{4H�CH��t��C4�1����fD�rH��1�H��论��D�c<[A\]�D��UH������]H��骶f.���UH��ATSH���M)���
�9*H��9*����H�H�3E1�E1�H�
a	fHn�H���fHn�H�����1�fl�H�C0H�=��1�Cj@j@jjj�Q��H��(H�3E1ɉ!9*E1�1ɺjH�=�1�jj�#��H��H�3E1ɉ�8*E1�1ɺjH�=`�1�jj���H�� A��H�
M�H�K�H�5\���8*H�=\�����H��1�A�H��7*A����H�D�H�5U�h�H�=V��&��A�1�A����H��7*H�B�H�5T��$�H�=T��&��H��7*��!��A��H�D�H�5P�H��H�=N���-��H�P7*�+��A��H�8�H�5D�H��H�=���-��H�)7*���A��H�7�H�5�H��H�=	��i-��H�7*���A��H���H�5�H��H�=
��:-��H��6*���H�5�A��H�=�H��H���-��H��6*�C&��A��H�ֳH�5�I��H��H�=���,��L��A��H�֐H�5�H�=��H��6*�,��L��A��H���H�5��H�=ːH�^6*�,��H�Z6*�A��H���H�5̐H��H�=���Z,��H�36*�~��A��H�q�H�5��H��H�=���+,��A��1�H���H�5��H�=b�H��5*���A��1�H���H�5�H�=��H��5*�Y��A��1�H���H�5o�H�=�H��5*�0��H��5*�(��A��H�W�H�5k�H��H�=o��+��A��@1�H�j�H�5b�H�=l�H�e5*����A��@1�H�y�H�5[�H�=l�H�D5*����H�@5*�-��H�=@�A��H���H��H���+��H�=E�1�A��H��H���H�5*���H�5*�%��H�=%�A��H���H��H���*��H��1�H�5#�H�=(�A��H��4*����H�߾H��4*XZH�e�H��3*[A\]�F��fDH�5�4*H���!�������ff.����UH��ATH��(dH�%(H�E�1�����I�ԉ�H�2�Hc�H�>��@H�E�dH+%(�H�wXL��L�e������@H�E�dH+%(��H���L��L�e�������H�E�dH+%(����L��L�e�����f.�H�E�dH+%(��H��L��L�e�����f�H�E�dH+%(�h��L��L�e���*��f.�H�E�dH+%(�8��L��L�e�����f.�H�E�dH+%(�H���L��L�e�����f�H�E�dH+%(��H���L��L�e�����f�H�E�dH+%(��H���L��L�e���y��f�H�E�dH+%(u|H���L��L�e���M��DH�E�dH+%(uTH���L��L�e���E��DH�E�dH+%(u,H�wxL��L�e��� ��H�0����H��H�E�dH+%(t��0��H�0��$��H��H�E�dH+%(t���H�E�dH+%(u�H�wL��L�e�����H�0H�u�H�U������u�L���4��H�E�dH+%(u�L�e����H�0H�u�H�U�����u�L�������f.�H�E�dH+%(�H���H�w@L��L�e���<��@H�E�dH+%(� ���H�w8L��L�e�����@H�E�dH+%(�����H�w0L��L�e������@H�E�dH+%(����H�w(L��L�e������@H�E�dH+%(�����H�w L��L�e�����@賨H��H�E�dH+%(�d����s����H��u�H�M�H�8�N���H�M�I��H�H�8�<���H�M؋u�ATPL�
*A�g1��qH�!�1�H�
��V����H�� �h���ff.����UH��AUI��ATI��SH����wH�
1���Hc�H�>��I�$�u�H�8���I��I�EH�8���ATL�
�~1�PA�H�
�1�A�u�u�H���V����H�� H�e�[A\A]]��H�PI�D$PH��t
H�U����H�U�H�����H�����H�����I�D$PH�e�[A\A]]�f�H���(��A9�$t�A��$H�5�.*H�e�L��[A\A]]�m��DH������A9�$�X���A��$H�5�.*�H������A��$H�e�[A\A]]�DH�����M��$�H��L9��
���H��tH�����I��$�M�����H�e�L��[A\A]]��@H�����H�e�L��[H��A\A]]�#����ff.���H�EH�nfHn�fHn�H��fl�H�
"fHn�GfHn�H�fl�H�G0G ����ff.����G|�@���ff.����ff.���Hc�0*H�H�G �ff.�f����ff.���UH��SH��H��HcA1*H�H�8H�H��t���H�+1*H��H�]�H�@(��ff.�f���UH��SH��H��H�H�CH��t��H��0*H��H�]�H�@(��D��Hc�/*H�H�G�ff.�f���H��tH�G(H�H�GH��H��HD��ff.�f����G0�����ff.����ff.����ff.����ff.���Hc�.*H�H�G�@��fD��UH��SH��H��H���H�{ �~�H�{(�u�H�{0�l�H�{8�c�H�{P�Z�H�{X�Q�H��/*H��H�]�H�@0��ff.����UH��SH��H��H���H��H�]���
�f.���UH��SH��H��H�P���H�{X���H�.*H��H�]�H�@0��f���UH��SH��H��H�(H�C(H��t��H��-*H��H�]�H�@(��D���ff.���Hc5-*H�4H�2-*���@��H�5=**H���-��ff.�f���H�5%**H���
��ff.�f���UH��ATI��SH�~�	���L��H������H��1�[��A\H�5�1�]�
��D����H�z0�����������UH�~1�E1�E1�1�H��H��V����XZ�����UH��SH��H��H�H��t$H��1�E1�E1�S1ɾ�~���H�{�e�XZH��H�]���6�fD��9�}1��DU����1�H���)��1�]�D����1��@U1�H��S����H�������x�tH�]�1������1�H���1����H�]�1��Ð��1��!��DUH��SH��H�����H��1�������H��&*H��E1�1�E1�L��A�H�t�D��H��H9�t,�A�@�u� u����H��A��H9�u�fD�f��f��9�O�A9�DO�A9�DO��A*��Y
S��A*Ѹf�9�OȍD
��*���X��
/��X�f/�wf/%��
vH�]����A���B�����H,�H�]���Df��1�f(��@��UH��H�J�H��ATSH�M�H��H�5��H��dH�%(H�E�1��u����t�M��u"H�E�dH+%(��H�e�[A\]�fDH�����I��H��t`H���p��H��H��t(H�57�H����H��u�H�5-�H����H��u�H���u�1�E1�jE1�1�L�����XZ�t���fDH��� �I������fD��LJ������uhUH��AUATSH��L���M��tBHLJ�M��I�]H�{�H�CH��tH�{��H����M�d$M��u�L�����H��1�[A\A]]�1��ff.�f���UH��ATI��SH�U�H��H��dH�%(H�E�1�H�E�����t/I��$�H�����H�E�dH+%(upH��[A\]����H�}����s����tH�}����@�{�H�}����K����u�H�E�H���1�H�H1��
�������UH��AUI��ATI��H�u�SH��H��H��(dH�%(H�E�1�H�E��s��H��H�U�E1�H��H�E�A�1�PL��AUj�<�H�� ��u4H�u�H��H�E����H�E�dH+%(u H�e�[A\A]]�fD�H�����������ff.����UH��AWM��AVI��AUI��ATI��SH��H���7��H����H��H�H��tH90tH���V������M����M��t&���H��I�$H��tH90tL���"������H��L��L��L���I��H�5b���H��H����H������H��tZL���r��H�"*H��H������H�5i���H�����H��tH��H��[A\A]A^A_]�v�fDH��[A\A]A^A_]ÐH�5тH���1��L���H�
����1�H�5�~�n��L���H�
p���1�H�5�~�M��L���H�
O���1�H�5�~�,��ff.����UH��AUI��ATI��SH��H�����H����H��H�H��tH90tH����	����tl1�H��L��L�����H�5����H��H����H�����H��t]H�5�?H������H��tH��H��[A\A]]�G��H��[A\A]]�DH��H���H�5��1�[A\A]]���H�5��H������ff.�@��UH��ATI��SH��H��H�u�H��dH�%(H�E�1�H�E�������t,I��$�H������H�E�dH+%(uTH��[A\]�@��H�}����c�����tH�}����@H�E�H�u��1�H�H1��������ff.����H��H���A���UH��AWAVAUI��ATI��H��H��SH���.�I��$�H��H������H���C�����H��I����I��$�L��H���p���M��tsL����H��tfL������L��H�E��Z���L��I�����H�M�H��*L��H������M��L��H��I��H�
v���H�����M��t(L���a���M��H�
>���L��1�H�����H��H��[A\A]A^A_]��ff.�UH��AWAVI��H��L�}�AUATSH��dH�%(H�E�1���H�E�H��I����1�L��H��I���n�H��tiH��H���~�L��I�����1�L��H���v���L��I����H������L���{�L���S�H�E�dH+%(ucH��L��[A\A]A^A_]�f��;��H�}�����+�����tL��E1�����f.�H�E�H�m��1�H�H1�������|���ff.����UH��ATI��SH�BH��H�x����H��t5H�HH��t�5�!*1�H��1���H�CL��[A\]H�x����fD[H�L~A\1�H�5u�]�/�ff.�@���5� *H��1�1��:�f.���H�`tH����1�1���U1�H��ATSH���Gx���H�5����H�C`I��H���2�L���J�H��t]H��H�5�}����H��H�5�}����H��H�5�}���H��H�5�}����C|1�1��5�!*H��[A\]�|�@H�5A�L���i�����H�`tH�F��1�1��0�U1�H��ATSH���Gx���H�5����H�C`I��H���b�L���z�H��t]H��H�5�|���H��H�5�|���H��H�5�|����H��H�5�|�����C|1�1��5� *H��[A\]��@H�5��L��������UH��SH��H�����H�� *�� *��u*H��H���H�H�CHH�Z�H�C(H�]���H�5� *H���A���ff.�@��UH��SH��H���;��H�\ *�N *��u"H�sH���H�E�H�C(H�]���H�5! *H����������UH��SH��H�������u*��u	H�]���H��H�]�H�5Z*���@��UH��SH��H�������*��uH��H�C(H�]���H�5�*H���Q���ff.�@��UH��SH��H���K��H��*�~*��uZH��%H���H�C0H���fHn�H�
�%fHn�H�9'fl�fHn���fHn�fl���H�]���H�5*H�����ff.�@��UH��SH��H���
����*��uH��'H���H�]���DH�5�*H���Y������UH��SH��H���[
���u*��uIH�Z*H�C(fHn�fHn�H�R,H���fl�H�@-H�����H�]���@H�5!*H���������UH��1�H��SH��Hc*H�H�;���H�;H�]���^��ff.���UH��1�H��SH��H��H���H�{H�]���$��@��UH��AVAUATSH���9�H���1��H��I��H��*�PHL�+Lc%*I���I�H��t(���I���1�I�$[A\A]A^]�y���f�L���h�1�H�5�I���H���@�I���H��tH���I����fDUH�=�xH��SH������H������H��H��L�
��jH��A���H�
�������H��H���Y�����*H��H�]���f.�UH�=bxH��SH�����H���c��H��H��L�
�jH��A� ��H�
����I��H�]���UH�=-xH���P���H��L�
�UA�`jH�ƺ��PH�
�������f�UH�=�wH��SH������H��L�
 �A� jH�ƺ��PH�
������8H��H���_�����*H��H�]���UH�=�wH��SH�����H��L�
0�A� jH�ƺ��PH�
T�_���XH��H������u*H��H�]���UH�=_wH��SH���K���H��L�
�A� jH�ƺ��PH�
�.���� H��H�������*H��H�]���UH�=wH�����H��L�
��A�(jH�ƺ��PH�
�������f���UH��AUATSH��H��L�'I��$�H��t���H�CH��[A\A]]����H������H���{�1�1��I��$����H�5^�H��I������I��$�L��1��!���L������I��$�1��*�I��$��q���ff.�f���UH��SH��H�����H�L*�B*��uzH�sH��H�31�H���H��E1�E1�H���fHn�H�=�u�fHn�H��fl�H�C01���jjj��H�]�H�� ��*��fDH�5�*H�������r���ff.����UH��ATSH������H��*��*����H��KH��H�
�y�fHn�fHn�H���fl�H�
6|fHn�CfHn�H��xfl�C(�d�H�߾	H��t�P�H�߾
H��t�<�H�߾H��t�(�H�߾H��t��H�߾H��t��H�߾H��t���H�߾
H��t���H�߾H��t���H�߾H��t��H�߾H��t��1�A��H��tH�5�tH�=Is�J��1�A��H��tH�5�tH�=8sH�>*�!��1�A��H��tH�5�tH�=sH�*���1�A��H��tH�5�tH�=sH��*����H��*���A��@H��tH�5�tH��H�=�t����H��*����A��@H���H�5�tH��H�=�t���H�߾H�r*H��*��L�%�*1�1�H��H�3E1�E1�jH�=kt1�jAT���H��H�3E1ɉ"*E1�1�1�jH�=Nt1�jAT��H�� �*H�e�[A\]�@H�5)*H������L���@����twUH��AUATI��S��H��H�H�8��I��I�$H�8��AUL�
a1�PA�)1�H�
�sA�t$H���S�i��H�� H�e�[A\A]]�f.�H�w(H�����@����twUH��AUATI��S��H��H�H�8���I��I�$H�8�{��AUL�
q`1�PA�\1�H�
_sA�t$H�\��S����H�� H�e�[A\A]]�f.�H�wH���4�@��UH��AUATI��SH����tgH�I�͉�H�8���I��I�EH�8����ATL�
�_1�PA�=1�H�
�rA�uH�ʂ�S�G��H�� H�e�[A\A]]��H����M�l$(H��L9�t�H��tH������I�\$(M��tL���,��H�5�*H�e�L��[A\A]]���UH�=erH��SH��(dH�%(H�E�1��L�H�����PjH��L�
R�A��H�
�����H�E�H��H���H�E�H�E��-��H�U�H��H���.��H�E�dH+%(u	H��H�]�������UH��SH��H����H�?@H��t�@����tgH������H��H��tlH���?��H�x�f�H��ts�H����t*���_��H����H��2H�Z@�q��R��u��H�]���@H�IqH�5��1��{��1���H��]H�5i�1��b��1���L�4qH�
P���1�H�56q����ff.�f���UH��AUI��ATI��H��SH��H������I9�u)H�5�H���Z����t>H��L��H��[A\A]]�1����H�Y�H�5��1�����H��1�[A\A]]�fDH�i�H�5Z�1����H��1�[A\A]]�fD��UH��AUI��ATI��H��SH��H���.��I9�t)H�b�H�5˹1��T��1�H��[A\A]]��H�5	�H�������tL��H���"��H���t�H��[A\A]]ÐH�9�H�5r�1����1�����UH��SH��H��H�~h���H���o�H��H�5�m�CHH�]�������UH��AWAVI��AUI��ATI��SH��H��H�G H�8H��t����H�C H�H�x���H�C H�x�=�L�{ L����L�s L��I��"��L�k L��I�F�r��5<*H��1�I�EH��1�[A\A]A^A_]�>�ff.���UH��SH��H��H�G H�x���H�C H�x���H�C H�8H��t�%���H�C H�H��*H��H�]�H�@0��DUH�=�nH��SH���;�H���S��H��H��L�
���jH��A�(�H�
��������H��H�����W*H��H�]���f.�UH��AWI��AVI��AUATI��SH��H���9tD���st6��i���x�,H�e��1�1������f.��yu�H�C�x�	�"L�����H����H�{ L����H��ur� ��L��I��H�C�@fA�E���L��I�E���L��I�E���H�{L��I�E����I�uH�{ L���9��H��L��[A\A]A^A_]��L��H���1�1���(��E1���H�m�1�1������f�A�|$�
��������A�|$�������H�a��1�1�����f�UH��AWAVAUATSH��H�u�G\��I��M��I����I��H�OP�����H�E�H��H�H9�����)�H9��O�H�CPH�C@H�@H��tL�(A�MJ�t!H��H�� v,� �U����H�{@�I��H�����A�M�U�H��A�T
A��P��H��I�|L��fA�LL����B�#A�EH��[A\A]A^A_]�@H�GPH� H��sH�5�k�=>H�u�H�M�H��H��A�����H�E�1��)���f�H�J�1�1�H�<��W��H��[A\A]A^A_]��UH��AVI��1�AUATS���A�H����L�%Hk�<H����H�CH�HH;KsyH�3H�K�H�H�C�A�VI����tG��"u�H����H�CH�PH;SsSH�\"I��f�H�CH�PH�SH��DA���u�H��[A\A]A^]���H�����H���a����H��jH�����H���=����d�����H�5ej1��M���D������UH��AWAVAUATSH��I�xtE1�H��L��[A\A]A^A_]���A�@I��I��I��I��L�����CA�}��A�E��ita��x��A�}��L����L��L��H�=�iH��1��N��I��L�����H�;L�KE1�H��1�L������S���@A�}u�L���a��L��L��H�=yi��1����I���fDI�8M�H1�E1��H�5<i�3�����.�����fDL��L��H�=i1����I���\���@��st#E1�H�
����1�H�5+i����fDA�}u�L���i���"H��I���I��H��tLL���<���L��H�E��@��L��L��H�=�hH��1��)��L�E�I��M9�����L���Q������@L�����L��L��H�=�hH��1�����I��������UH��SH��H���;���H��*��*����H��RH�xH�C0H���fHn�fHn�fl�����H�3E1�E1�P1ɺH�=+hj@1�j@j@j@jjj�6��H��8H�3E1�jE1�1ɺjH�=hj�*1����H�]�H�� �*���H�5�*H���Q���F���ff.����U�H��ATI��SH�����H��L� H��H�XI�D$H�x����L��H��E1�[E1�A\H���H�5mg]�g���UH�=��H��SH���;�H����H��H��L�
���jH��A�0��H�
n�������H�]���UH��AWAVAUATSH��H�$H��xdH�%(H�E�1�I���\�I�^@H��h�H�����s��I��H����H��x�H����H��p��wf�L����L��x��H��p�H������A��������H����1�H������������H��������H��h�H�����H�[H��tQL�#M�<$M��t	M9/�v���L��L��������c���I�?����H�̈�1�H��1��C��H�[H��u�H��h�1�H����H��1��/���I�NI�>1�PH��1�A�v8M�N(M�F �5�*�	��XZH�E�dH+%(uSH�e�H��[A\A]A^A_]��f�D��H��1�1���������D��H�Ƈ�1���������@���UH�=�H������H�5�]�����ff.�f���UH��AWI��AVI��AUATI���PSH��8H�U H�EH�M�H�M(L�mL�E�L�M�H�M�H�U�H�E����L��L� H���"��L��H�C���H�}�H�C �	��H�}�H�C(���H�}�H�C0����H�}�H�C8��H�5�)1�H�C@H���=�H�M�H�U�1�L���;�L�k1�H��H�CHH�5����L�����I�|$H��H�C�~��I�D$H������I�|$ tH��8[A\A]A^A_]�f.�I�|$H��t�H�I�\$ H�����H��I�D$H��8[A\A]A^A_]���ff.�UH��AVAUATA��SL�/H��M�u I9�tI�}H�����I�EH�sH�{���E�����H�5y�1��j��I����H�{HL����1����H�{���H�{ ���H�{(���H�{0���H�{8�p��H�5�)H�{@1����H�{@�5��H�{H�l��H���D��I9�t'[A\A]A^]�fDH�{H�������DI�E I�}�w��I�} u�I�}H��t�H�I�] H�����H��I�E[A\A]A^]�T���@��UH�H��H9x t1����1�]�@�5V*H��1�1����1�]�f.�����r���f���UH��SH��H��H�GH�8H�H��t	�X��H�CH�xH�@H��t	�.�H�CH�x H�@ H��tH�]�����H�]���f�����UH�FH��AWE1�AVA��AUA��ATI��SH��H��(H�E�H�FH�E�H�FH�E��fDA��E9�tR�L��H������H�uȺH������H�u��H�����A��u�H�u��H��A�����E9�u�H��([A\A]A^A_]�fD����UH��AVAUL�u�ATI��H��L��SH��dH�%(H�E�1�H�E��4�H�u�H��H����I�D$H�E�H�x0�.��H�}�H��t�0��H��u\H�E�dH+%(�H�e�[A\A]A^]�I�D$H�u�H�E�H�x0����H�}�H��������M��tL�����H�{�v���H���n����@I�D$�p�x���E1�L�CH�sH��I�Ń�D��A��H��A��AVE�����ZY���f���I�D$H�z�)L��H�x0�v��I�D$H�x(H�@(H��t����H�}�H���_����)���U���@M���@����C����]��ff.�f���UH��SH��H������!�)��uA�,�H�3E1�E1�P1ɺH�=_j1�jj���H�]�H�� ��)���H�5�)H���Q���ff.�@��UH��H��1�H��ATI��SH�X�Q�L����w��L������H�{ H�C H��t����H�{H�CH��t��H�{H�CH��t[A\]�y�f�[A\]�ff.���UH��AWAVAUATSH��dH�%(H�E�1�H�E�H���L�fI��I�|$�*���I�|$ I���}�I�|$ ���A��I�|$ �ډ��#,I�|$H�5�]I�����H��H����H�^jL��L�E�SL�
�]1�L��PH��]H��]P1���H�u�H�� H��tRL���
��H���½��M��tL���ս��M��tL���Ƚ��H�E�dH+%(ubH�e�[A\A]A^A_]�f.��L�������I�|$H�5A]����H���E���L�]H�
:��(1�H�5�\������f���UH��ATI��H��SH�u�H��H��dH�%(H�E�1�H�E�������u4H�u�H���(���H������H�E�dH+%(u-H��[A\]��L��舼��H��)H��H�������?��ff.�@��UH��AWA��AVM��AUATE��S��H��8�EH�M0H�}�H�U(1��u�1��E��E�EȉE�H�E �M�H�E����H�5����I��H���x��L�����H��������NA����D���H�����U�H�u�E��A��D��H�����I��H������L����j����1����o�H��H������H��H�E��h�H�M���L����U�H���
�L��襻��f��H���ZM�f(�����M����L���O��L��A������D��1������I��H���]��L��H�E����H�M���L�������L�����
ޛL���^M��Z�f(�艿��H���1��f�ɋM�f�I�ƋE��U�L��L��D)��*ȋE�)��*��^��^��Z��Z��0�L����L����L�����H����H��A�����D��H�߉��h(H��I���]�����H�5
ZH��I������H��H����H��H�6ZM��1�jL����H�ZL��SPH�)ZPH��YPH�u�1��
��H��0M��tL����H����M��tH�e�L��[A\A]A^A_]���fDH�e�[A\A]A^A_]ÐL�}�L���T��L��E1�������E�A������H�5q|L������e���H�5QYL�����H������D��UH��SH��H��H�H��t����C0f�H�CC H���)H��H�]�H�@0��fD��UH��AWI��H��AVAUI��ATSH��(�ủ�I���U�I�W(I�_ L)�I�OI��H�DH9����u�H��H�U�H�M��"�H�M�H�U�H��H��H)�I�H)�H�<���I�L��L��H��O��I�GMg(D��Ew0�u�L��L��M�g(B� �y�H��(D��[A\A]A^A_]��H�H����w<I�_ H9�sH��u�I�_ H9�r�H��H�����I�W(I�GH���8���D���I�G �����H)�L9�s�H��L��H�M�H)�L��	��L��L)�H��I����H�M�A���ff.�@��UH��AWAVAUATS��H���G09�F�2��)�9�Bڅ�uH����[A\A]A^A_]�L�I��A��L�����B�4#L��I�����L��M)�H��H��I�E(L)�H�PH�M�H)��z�H�M�A)]0��D��L��I)�Mu(����ff.��UH�=WH��SH���[��H����H��H��L�
���jH��A�8��H�
~���	���H�]�����UH��ATI��SH�u�H��H�U�H���?�dH�%(H�E�1���M��t�E��A$H��t	�E��H�E�dH+%(u	H��[A\]�����ff.��UH�=JVH��SH�����H�����H��H��L�
��jH��A�(��H�
N���9���H�]�����UH��AWAVI��AUATSH��H��8H�u��E�dH�%(H�E�1��H�H�u�H��H�E���H������H�����E�I��A�L�e��E�H�]��-fD�M��U��M��U�L��E1���I��H��tI�E�L��H��L���F���E��u��E��_E��E��E��_E��E�뵐�E��E�f�H�E�H��t	�m��(M��t
�u��A6H�u�H�}�L������H�E�dH+%(uH��8[A\A]A^A_]��,��ff.����UH��AWAVI��L�}�AUI��ATI��SH��(�E�dH�%(H�E�1����H�u�H��H����I�|$(�E�L��H�U�H�U��L���H�U�L��H���=��M��t
�E��AM��t�E��AEH�E�dH+%(uH��([A\A]A^A_]��k��ff.���UH��AWAVI��AUATSH��H��8H�u��E�dH�%(H�E�1��8��H�u�H��H�E���H������H�����E�I��A�L�e��E�H�]��-fD�M��U��M��U�L��E1��q�I��H��tI�E�L��H��L���6�E��u��E��_E��E��E��_E��E�뵐�E��E�f�H�E�H��t	�m��(M��t
�u��A6H�u�H�}�L���?��H�E�dH+%(uH��8[A\A]A^A_]����ff.����UH��AWAVI��L�}�AUI��ATI��SH��(�E�dH�%(H�E�1�����H�u�H��H����I�|$(�E�L��H�U�H�U��<�H�U�L��H�����M��t
�E��AM��t�E��AEH�E�dH+%(uH��([A\A]A^A_]��[��ff.���UH��AUATI��SH��H��8dH�%(H�E�1��5��L��H��I�����L��H�U�L�����H������H��t/H��L�e�@foE�H��L��)E�����H����H��H��u�H�E�dH+%(uH��8[A\A]]����ff.�f�UH�=9QH��SH���k��H�����H��H��L�
����jH��A�(��H�
~������H�]�����UH��AUA��ATI��SH��H������t-M��tL��H��蝹����uH���A����u}1��fDH���ȸ��H��H��u�~fDH��谷��H��H��thH���@�����t�����H��H�H��tH90tH���_����t�H��D��L��H��1�[A\A]]����H������H��[A\A]]�����1����UH�=PH��SH���+��H���c��H��H��L�
���jH��A�0��H�
��ٲ��H�]�����UH��SH��H������5��)H���)���H�?���H��E1�A�����H�C0H�
H�sfHn�fHn�H�vfl�H�
�ufHn�C fHn�1�H�]vfl�H���H��qH�5V\��H�=(Jh����H�߾H�����A��1�H��NH�5
OH�=O�J���H�߾H�����1�H��NH�5OH�=jTA������H��H�]��H��XY��Q���H�5q�)H���A������ff.����H��H�׃�tIUH��AUATS��H����t��u@�pHH�e�[A\A]]�&���fDH�pXH�e�[A\A]]���DH�pP����H�I��H�8�
���I��I�$H�8���AUL�
�81�PA��1�H�
!NA�t$H��[�S�Y���H�� H�e�[A\A]]�f.���UH��SH��H��Hc	�)H��t�H��tH�|�߽���H�]���D��UH��SH��H��Hc��)H��t�H��tH�|�����H�]���DUH�=�MH��SH���;��H����H��H��L�
ռ��jH��A�`��H�
������H�]�����UH��SH��H��H�0H��tH�s8H��uH�C0�q���H��H�]�1������f�H�C8����H�{0H�C0H��u�H��H�]�1�������UH��ATSH���-��H��)��)����H�1H�
�H�C0H��fHn�fHn�fl�C�8��H�3E1�E1�PI��1ɺjH�=cL1�jj�0���H�� H�3E1ɉ��)E1�1ɺATH�=EL1�jjj����H�� �r�)�!��H��nH�5-LA�H�=)LH���N��H�e�H�߾[H��A\]�&��fDH�5)�)H����������ff.����UH��SH��H�����H���)���)����H�O���H�58���fHn�H�,fHn�H���H�ٻ��fl�H�
���H�C(H������fHn�fHn�fl�C����H�=ZKA��@H��H��H�����H��H�]�H�,�)H�-�)���J���f.�H�5�)H���!����F���ff.����UH��AUI��ATSH����tgH�I�̉�H�8�7���I��I�$H�8�(���AUL�
51�PA�f1�H�
�JA�t$H�	X�S膳��H�� H�e�[A\A]]��H������H��tI�U��BH�e�[A\A]]��I�E�@�H�e�[A\A]]�f�����twUH��AUATI��S��H��H�H�8�z���I��I�$H�8�k���AUL�
a41�PA�y1�H�
�IA�t$H�LW�S�ɲ��H�� H�e�[A\A]]�f.�H�wH��H���@�����UH��AUATI��SH��Hc
e�)��t`H���H�8���I��I�$H�8�Բ��AUL�
�31�PA�=1�H�
�kA�t$H��V�S�2���H�� H�e�[A\A]]�H�e�H�tH��[A\A]]驹��f���UH��SH��H��H�GH�8H�H��t����H�CH�xH�@H��tH�]���y��f�H�]���f.���UH��SH��H��H�GH�8H�H��t訧��H�CH�xH�@H��t���H���)H��H�]�H�@0��f���UH��SH��H��Hc	�)H�|����H��)H��H�]�H�@0��f.���UH��SH��H��H� ���H���)H��H�]�H�@0��f���U1�1�H��SH��Hc��)H�
�)H�1��f���H�CH�]���ff.����H�5�J������UH��AWI��H��AVL�u�AUI��ATI��SH��dH�%(H�E�1�H�E����L��L��H��H�����H�}�t�b���H�}�����2�������L������1�L��L��L�����H�}�I��t�#���H�}��������toL�����M��tR1�1�L��覷��L����)���L���ѥ��H��tH���ĥ��H�E�dH+%(uSH��[A\A]A^A_]�fD1�L�������@H�u�L��褩��M��t�L���w����DH�u�L��脩�������ff.�f���UH��SH��H���w(����H�{H��t'H�s ����H�{H�CH��t�
���H�C H�;H�H��t��H�{0H�C0H��t軤��H�{H�CH��t襤��H��H�]��阤���H�?踫���C(�g���ff.����UH��H��ATSH���
��H��I���߶����u#I�|$0tH������H��[A\]�O����[A\]���UH��AUM��ATSH��8H�]dH�%(H�E�1�H��聶����t%H�E�dH+%(��H��8[A\A]]��H���p��H�x0I��t�H�5�DL��H�M�1�H�U�L�M�L�E�����I�t$0H�}��g����u�H�}�H�5�D�S����u�H���Ҽ��H���z����h���DL�e��w���H�
�f1�H�߉�M��1�������ȿ�����UH��AUI��ATH�U�SH��dH�%(H�E�1�H�E�����L��I�����H�}�H��uwH�x0��H�P0H�5�C1�L���7����Ct9H�}�H��t��M��tL��覢��H�E�dH+%(unH��[A\A]]�@L���ۻ��H�}�H��u��H�HH�f1��1��7���H�u�L��H�E��s���L���K���H�}�H���v����v���贾��L�ICH�
���.1�H�5FC�S��UH��AWA��L��AVAUI��L��ATSH��8H�}�1�H�u�L��dH�%(H�E�1��B��1�H�U��H�E�I��H�E��O���H����H���.��H�u���������������2�8�(��H�}�I���\���H��I�F����E�~I�E����H�����L��L���~���M��t$L�����1�L��L��I�FH�5�����I��I�F H�=&B�Y���ATL�M�H��H�
����L�E�H�DBQH�5aBH�
�dAUj�jP���H�}�H��0覠��H��辠��H�E�dH+%(�H�e�[A\A]A^A_]�@H�Q���jE1�H��ATL��AH�
�APH�adH�5�Aj�3���H�� A�F(����fD��A�ʼn�����D��I���)��A���A���L��M��D���H�
�c1��H��H�}����H������M���@���L������3����H�u�L������H�}�軟����f��ۢ��L��H�
�@��1�����H�}�芟����#�����UH��AVAUL�m�ATI��SH��H��PdH�%(H�E�1����H��L��I������H��L��L������H�U�H�u�L���c���H�]�H�U�L��H�u��߬��L��H��L�e�����#fD�]��U��M��E�H�}����L��H��������u�H�E�dH+%(u
H��P[A\A]A^]��K���ff.���UH��SH��H�����H�\�)�N�)����H�_�H�
�fHn�fHn�H��fl�H�ܯ��H�
����fHn���H���fHn�fl�H�C0H����fHn�fHn�fl�C ���H�5�?H�=�?A��H��H������H��H�]�H���)H���)������H�5��)H������6���ff.����UH��AWAVAUI��ATI��SH��xdH�%(H�E�1�Hc?�)H�L�sL������L����p���������p���(�f�.��o�
�}�af�.��|��}�n�L�e�L�u�L��(�L��L�m��)�p����Ƕ���L��L���������H�}�f�)E��̝����t�H�u�H�{���H�u�H����H�8L�}��"���f��f�L���*M��\K�*E��\C�N���H�}�1�1�L�E�L������M��E�L���h��(�p���YE�L��H�}�)E����L��L���B�����M���H�E�dH+%(��H��x[A\A]A^A_]��H�}�L���t����M��E�H�}�������@L����l����x�����l���(��^�p����w���L����l�����p���踝����l�����p����^��a����
���f.���UH��AWAVL�}�AUL�m�ATSH��XLc%�)H�}�I�L��dH�%(H�E�1�I�t$�)���H�E�H�E��.�I�vH��u^I�v H��ukI�v(H��u|I�<$H�����H�u�L��L������tvL�u�H�]�I�vI�>H��t�I�F�=���I�vI�>H��t�I�F�$���I�v H��t�I�F I�~�
���I�v(H��t�I�F(H�����o���@I�|$����H��)H�}��P(H�E�dH+%(uH��X[A\A]A^A_]�轶��ff.�f�UH�=�;H��SH���{���H������H��H��L�
�jH��A� ��H�
.����)���� H��H���ɸ�����)H��H�]���f.�Uf�H��AWAVL��h���AUL�m�ATL�e�S�H��H��X���dH�%(H�E�1�Hc'�))E�H�L��XH�pL�x)�p����E���D1�L��L���C�����t?H��h���L��H�8�}����tfoU�1�)U���@H�u�L��1�H���w����Df�f��L���*E��*M�覤��f�f��L���*E��*M�����H��p���L��载����t1H��X����}���H�E�dH+%(u+H�Ĉ[A\A]A^A_]�@H�5)�)H��X����E�������ff.���H�����@��駱�����H�G(����H�G����H�G0��UH��AVA��AUATI��SH��H��L�odH�%(H�E�1�I�}���H�U�H�u�H���\���H�{D�E�E��M�1�1��efnM�1�H��fnE�I�E(L��H�D��fb�fA�E0���H�5���H��H������H���k���H�E�dH+%(u
H��[A\A]A^]��ʳ��f.���UH��AWI��H��AVAUI��ATSH��譸��H�XI��H�;���H��1�E1�AU1�L�
�����L��I������{XZtB�sL��L�������56�)L��H�K(1�1�螦��H�e�L��[A\A]A^A_]�8�����K0�S,�s(I�|$D�KD�C4�51�L��L��H�&������H�5���H���������UH��AVI��H��AUATSH���ӷ��I��H�@H�8����H��1ҾSI��L��L�
����E1�1��!���L��虿��I�|$XH��ZH�e�[A\A]A^]�ff.���H�G@����H�Gh����H��1�H��H�5q*������UH��AUI��ATI��SH��H��(H�0dH�%(H�E�1�辥��H�U�H�u�H���>���M��t	�,E�A�$H��t�,Eԉ�E�%�\A�EH�E�dH+%(uH��([A\A]]�虱��f���H���@��H����@��UH�0H��菪����t]�f�]�Z��f.���UH��AVI��H�=s6AUH�M�H�U�ATH�u�SH�� dH�%(H�E�1�H�E�������L�m�Lm��V���H�]�I��L9�s'f.�H��L���u���H���M���H�\L9�r�1�L���Y����H�=ͦ���L������1�H������L������L����������H���ʪ��I�4$H�>���詔���8蒫��H��5�1�H��1�芝���L�����H�}�脓��H�}�H��t�Ƙ��H�E�dH+%(u?H�� [A\A]A^]�f.�H�E�1��H�vWH�H1��#���H�}�H��u���ï����UH��AWI��AVAUA��ATA��SH��L�w@H�0�%���H���]���H��E��t]D��H������E��xD��L���*���H��H��诡��H��E1�E1�1�H�M���H�5�4�1���H��H��[A\A]A^A_]��L�����A���ff.�������@�������t*UH��SH��H�������u
�����t!H�]���H�94H�5�{1����H��H�5�����,1��ڼ��H�5SV���H�]�����1������UH��AVI��AUI��ATI��SH�������H���L�0H��L�hL�`������H�����u
�����t[A\A]A^]�DH��H�5�����,1��B���H�5�U�����[A\A]A^]閻��fD��H��H��H����*���f.���H���� �����H��H��H������f.���H��������5Z�)1�1��͠��ff.�f����5>�)1�1�魠��ff.�f���H���)H��tH���)��UH��SH���)H��H��踚����uH���)H�]����諳��H��H������H���)H�]�����UH��AVAUATSL�7I�����A��I��H��E��u:D��H������A����I���H��uDH�����H��[A\A]A^]���@1�D���Ζ��L��H��H����A����I���t�H��1�����H��[A\A]A^]馏��fD[H��1A\1�A]H�5kxA^]�3�����Hc��)H�<�l���ff.������I���י������Hc��)I�H���K���ff.���I������D����A�ҙE������HcQ�)I�H���͚��ff.�f���H�
�)H��tH��)��UH��SH���)H��H�������uH���)H�]�����K���H��H���@���H���)H�]�����UH�����1�]H��1�馎��fD��H�%�)H��tH��)��UH��SH��)H��H���X�����uH���)H�]�������H��H��谪��H���)H�]�����UH�����1�]H��1�����fD��UH��SH��H���[���H��t&H��H�H��tH;0tH���ΰ����t
H�ChH�]���H��QH�5bu1��+���1������UH��SH��H�����H��t&H��H�H��tH;0tH���n�����t
H�CpH�]���H��QH�5�t1��ˏ��1������UH��ATH�����yI�ԉ�H�
nHc�H�>��H�wXL��L�e�������x@��@��L��L�e���ϡ����wD��t�1��x@����DH�G01�H��tɀ8@���@H�G81�H��u�����L��L�e�H���飪��������f�H�wL��L�e�H��H��9�HD��Q����H�w ��f.�H�w(��f.�H�w0�f.�H�w8�f.��w@L��L�e�����wDL��L�e������wHL��L�e��鐪��L��L�e�H�5G9��̠��@H�wPL��L�e��鷠���H��u�H�M�H�8讕��H�M�I��H�H�8蜕��H�M�u�ATPL�
�A�1��qH��91�H�
�)V����L�e�H�� ��ff.���UH��AUATSH��H�����H����H��H�H��tH;0tH��������tr�Sx���7H�{`�LH�{h�3���I�ċCx��tpL�k`�CxH�C`����1�L��L���ϔ��L���w���H���[A\A]]��H��NH�5Rq1����1�H��[A\A]]�fD�CD��taH�{p訹��L��H���=����H�5m,��u"H�=z,�a���H��t,�8t'�H�5v,1��t���H��H���ٙ��1��DL������H�5�&H�߉CH覬���Cx����fD�L��諧������fDH��+H�5�p1��+���1��+���@H��+H�5bp1�����1�����@��UH��ATSH������H��tXH��H�H��tH;0tH���P�����t<�Cx��tUL�c`M��tlH�C`�Cx��thL��1�1��<���[L��A\]���H�AM[H�5�oA\1�]�w�����C|��u	[A\]�@H��[A\]�4���@H��*��L��1�薦��[L��A\]�z���f.���H��)H��tH��)��UH��SH���)H��H���h�����uH���)H�]�����+���H��H�����H���)H�]�����UH�����1�]H��1��&���fD��H�G H�8�@�����H�G H�x�O���ff.�@��H�G H�x鏗��ff.�@��H�-�)H��tH�!�)��UH��SH��)H��H��舑����uH���)H�]�����;���H��H�����H���)H�]�����H���)H��t��UH���o���1�H��1��3���]H���)�f.����G\���‰���8�tZU��	�H��SH��H���G\��u�X��uCH�]���DH��H�5F��茊��H�5
K�CXH�]����馰��fD���CXH�]��鷺������Ƕ�����UH��AUATSH��H������L�%H�)L�-�)H�CL��L���ں��H�C ��L��L��L�%-H�C(軺��H�C0�ҥ��H�C8虇��L��H�t3H��H�C@H�5�$�l���H�C�xu@L��H�XJH�5E(H���I���H�C�xu>�Z���fHn�fl�CHH��[A\A]]�L�;JH�
$l��1�H�5�$葴��L�JJH�
l��1�H�5}$�p�����UH��2H��ATSH��H� �H��t%I���խ��L��H��E1�[H��A\1�]����fD[A\]�������u
��@UH�����H�5u'H������]�f�������tB����t8UH��SH��H�=��)t-�p������)�E���H�]�H�5�I�H���!������H�'H��詠��H���)H��u�H��H�[I�1�����D������u
��@UH������H�5�&H������]�f���UH��ATSH��H��H� �U�H����H��t I���n���H�M�L��H��H��A�覵��H��[A\]�ff.�f���UH��ATSH��H��H� H�U�H�n*��H��t I���
���H�M�L��H��H��A��E���H��[A\]�ff.����UH��AVAUI��H��$ATSH��H� �[�H��t6L��I�����I��裫��M�FL��L��H��H��[A\A]A^]�ִ��fD[A\A]A^]����UH��AUATSH��H�����iuT�yuNH�����I��H��t|� 褔��H�{(�`�H��I��L�(�~���I�uH�{0L��H��[A\A]]���@��xt#H��H��G1�1�[�A\A]]�܋��@�yu�H���z���I��H��u�H��[A\A]]����UH��S��H��H��H�0��H��t�H�XH�]���ff.�@��UH��SH��H��(H��H�0�`�H��t�HH�XH�]���ff.�f���UH��AVI��AUI��ATI��SH����{���H�{8L�0H��L�hL�`[A\A]A^]�L���ff.����UH��AWAVAUATI��SH��dH�%(H�E�1�贩��A�D$\�I��I�D$8�H��t'1��H�L��H��H��H�p�I�D$8;Xr��m���L)�H�E�I�D$(�P����E1�L�=<F�3f.���x��1�L���1��&���I�D$(I��D;hsgH�J���S��t�H�3H�N�	��iu���t�{9{t�H��H�KA�L��L��I�������C�K�CI�D$(D;hr�f.�I�D$H�M�L��L��A�H�H�P�߱��H�E�dH+%(uQH��[A\A]A^A_]Ð��tH�{H9{�<���H��H�KA�L��L��蕱��H�C�KH�CI�D$(�����ڛ��f.���UH���3����]�ff.����UH��AWAVAUATSH��XH�u�L�oHH�U�dH�%(H�E�1�H�G@H�H�E�H���I��f�H�E�H��H�E�����E1�L�`��D��A�I�I�OH�	H��H�A�8�.�0��i���x��A�H�]�H�M�A��
�H�߉E�耬���u�H���5���H�M�H�AH�qH��L�E�H��H�E�L���H���~���H�E�D;0s7f�A�FH�E�A�V)E�A�f���G���H�E�M�,A��D;0r�H�E�H�@H�E�H�������H�E�dH+%(��H��X[A\A]A^A_]����xus�xumI�H�]�H�M�A���(H��H�E�褫��H�u�H���(���H�M�H�A����H�]��@H�M�H�߉U��p���H�M�D�u�H�A�����suS�xuMH�]��@H�M�H�߉U��5����U�H��L�H��H�U��0���H�}�����H�M�E�tH�A���DA��H�]�����O���ff.�@��UH��AWI��1�AVI��AUATSH��(H�U����I��H���fH�@H�PI;U�IEA�[ fD�I�EH�PI�UI�U�DI�G�x���L��L�u�M��E1�I����DM���gI�GH�PI;W��I�,
  I�GH�PI�WI��DI�V1�H��H�5�AL���`���H�}�tBI�GH�PI;W�7fo�\IH�c": trueH�HI�GH�PI�WI��DI�GH�PI;W��I� }f�0I�GH�PI�WI��DI9^tH���8{��I�EI��D;`�H��"N�4�I�^H��辂��H��tH��豮��H��I�vI�}0�A���H�E�E������I�V1�H��H�5�@L���l���H�}�tuM�������H�5�1�蹃���H�5�1�覃���K�����H��H�����L���U����*����H�H�����L���5�������M�������D�H�XH�����L�������H���L�u�M��M���0I�EH�PI;U��IE� ]f�I�EH�PI�UI�U�DL���5���H��H�����L�M�H��(H��L��[H��A\E1�A]1�A^A_]�ŋ��D1��H�5�蝂��I�V1�1�H��H�5{?����H�}���������fD�H�lH�����L���%����c����H�IH�����L������I�GD�HE����������H�51�����I�GD�PE�������@�H�5�1��݁��������UE1�1�H��AWAVL�u�AUM��I�պATI��SH��H��H��(dH�%(H�E�1�H�u�H�5�H�E��E�耊����u$1�H�U�dH+%(uiH��([A\A]A^A_]�@L�}�H�5����L��L������H�u�H��tL���~����E1�1�M��H�5H��������������}���ff.�f���H�e�)H��tH�Y�)��UH��SH�D�)H��H�������uH�-�)H�]����諯��H��H���@���H�	�)H�]�����UH��AVAUL�u�I��ATI��SH��dH�%(H�E�1�H�E��ם��1�L�������H��tV1�M��E1�1�H��L��H���n���H�u�I�D$(H��tL���}��H���w��H�E�dH+%(uNH��[A\A]A^]�@H�u�H��tL���O}����D���H��<1���1�����H�E�H��t�H��������ff.���UH�����1�]H��1��v��fD��UH��SH��H��H�H��t1�H�5����詝��H�CH�{ H��t
����H�{(H��t
�p���H�C(H�]���f���UH��SH��H�����H���)H��H�]�H�@0��fD��UH��ATA��SH������H��t5H��H�H��tH;0tH���ݘ����tH�{ H��t0[D��A\]�d���@H��;[H�5\A\1�]�'x���H��;�����H�Ŵ)H��tH���)��UH��SH���)H��H���X����uH���)H�]�����K���H��H��谑��H�i�)H�]�����UH��AWAVAUI��ATI��SH��H��8H�EL�}H�M�L�E�H�E�H�E L�M�H�E��M���H����H��H�H��tH;0tH��輗������L�sM��t+A�<$t$H�E�I9���M��tZ�mw��A��H�i��M��tC�Vw��A��H��:H�EA�#H�U�H�
7���L��1�H��8H��[A\A]A^A_]�<���@H��8[A\A]A^A_]�f�H��8H�m:H�5Z1�[A\A]A^A_]�}v��DI�>t*M��t���v��A�A��H��:H�E�m����L��考��M�nH�}�I�M�n����H�}�I�F����H�M�H�u�L��I�F H���Ȣ��H�5a���I�F0I��H���rz��L��芁��H��tuI�~01�H��L�-�����`���H�u�1�L��H������L��I�F(I��H���-z��L���E���H��tHI�~(H��1��"���I�~(H��8H�5�/[A\A]A^A_]�D���@H�5�9L���~���w���@H�5�L���y~������UH��AUI��ATI��SH��H������M����H��I�$H��tH;0tL����������貛��H��H����H�H��tH90tH���Q�����t}H�5&���H����������I�D$H�8H�H��t
��q��I�D$H�xH�@H��t
袙��I�D$H�x H�@ H��t臙��H��L��H��[A\A]]题��f�H�dH�5�W1��;t��H��1�[A\A]]�fDH��7H�5jW1��t���֐H��8H�5RW1��s���f���UH������1�]H��1��fq��fD��H�u�)H��tH�i�)��UH��SH�T�)H��H���{����uH�=�)H�]�����k���H��H���p���H��)H�]�����UH��AWAVAUATM��SH��H���u�L�u�U�L�}�M�D�E��[���H����H��H�H��tH;0tH��芓������譔��H��M��tuI�$H��tH90tL���_�����t[L�kI�}��M���R�s��L��H��1�A��H��7H�
E���A�H�EH��1�[A\A]A^A_]����fDH�H��H�5~T1�[A\A]A^A_]�]r��DH�97���L��1�L��H�����H�5���I��H���v��L���}��H���51�H��L��蘏��L���o��fnM�fnU�fnE�fn]�I�Efb�fb�fl�AE(�Œ����tnH�{E�E4E1�A�M0A�U,A�u(���5d�)1�H��1�I�M(��~��1�H��L��H������H���H�5���H��H���F���H��uqH��[A\A]A^A_]�f�I�}���I�}I���k��L��H���@���H�����L��H��E1�H��NE1�H�.��H�5�I�EH��[A\A]A^A_]���H��H��[A\A]A^A_]�:n��f.�H�5!6L���z�����ff.����UH��AWAVM��AUI��ATI��SH��H���u�����H����H��H�H��tH;0tH�������������H��M��tqI�$H��tH90tL���ː����tWL�{I���M��t3�p��H��L��L��A��H�*5H�
S���H��PA�1�肋��ZYH�e�[A\A]A^A_]ÐH�|H�e�H�5�R1�[A\A]A^A_]��o��DH��4���L��1�L��H���h���H�5��I��H���t��L���.{��H����1�H��L������L��E1��ml��I�G�E̅�A���K�����t7L��D��H������5��)H��1�H�e�I�O(1�[A\A]A^A_]�U|��DI�?�H��I�?I���-��L��H������H��躊��E�gL��H��A�GH�e�E1�E1�[H����H�5~A\A]A^A_]遃���H�5tL����w���'���ff.����UH��AUI��ATI��SH��H�����H����H��H�H��tH;0tH���������L��1�L��H������H�5����I��H���r��L����y��H����1�H��L��訋��������tH�{H��L��[A\A]]��f�H�[H�;���H�;I�����L��H���Β��H��膉��H��L��H��E1�[E1�A\H����A]H�5R]�]���DH��H�u2H�5P1�[A\A]]�ym��f�H�5�2L���v���7���ff.����UH��AWM��AVA��AUA��ATI��SH��H���W���H����H��H�H��tH;0tH��膍������L��1�L��H��讙��H�5����I��H���\q��L���tx��H����H�{E1�D��A�H�tJ�D��D�o(D�w,H�G0���L���.���M��tH��L��[A\A]A^A_]��i��H��[A\A]A^A_]ÐH��H�=1H�5nM1�[A\A]A^A_]�=l��DH�5
L���Yu���Y���@��UH��AVI��AUI��ATI��SH���0���M����H��I�$H��tH;0tL���^�������葒��H��H��taH�H��tH90tH���4�����tHH�5��H��������t}L��H��M�d$�����t6M��tI��(M�e[�A\A]A^]�fDH�|�H�5*N1��Sk��[1�A\A]A^]��H�!0H�5N1��+k����f�H��0H�5�M1��k���f���UH��AWAVI��AUI��ATM��SH��H��H�}�L�L�M����H�}�H��H�H��tH90t
�5�������h���H��H����H�H��tH90tH����������H�5���H���Й������H�uH��謎��H����M��t�AG<�AI�@I�G@M��t:I�}M��tI�WHI�$H�M�H��t
�AGP�H��[A\A]A^A_]��H��t�H�E��:g��H�E��@H��H�5"L1���i��1�H��[A\A]A^A_]�f.�H��/H�5�K1��i��1����H�q.H�5�K1��{i��1�����UH��AVI��AUI��ATI��SH�����M����H��I�$H��tH;0tL��讉���������H��H��taH�H��tH90tH��脉����tHH�5����H���Q�����t}L��H��M�d$�M�����t6M��tI��(M�e[�A\A]A^]�fDH��H�5zJ1��h��[1�A\A]A^]��H�q-H�5RJ1��{h����f�H��.H�52J1��[h���f���UH��AVI��AUI��ATI��SH���`�M����H��I�$H��tH;0tL��莈���������H��H��taH�H��tH90tH���d�����tHH�5I#H���1�����t}L��H��M�d$�-�����t6M��tI��(M�e[�A\A]A^]�fDH���H�5�H1��g��[1�A\A]A^]��H�Q,H�5�H1��[g����f�H��-H�5�H1��;g���f���UH��AVI��AUI��ATI��SH���@�M����H��I�$H��tH;0tL���n�������衍��H��H��tYH�H��tH90tH���D�����t@M����H�5`���H����������L��H��������uQ[1�A\A]A^]�fDH���H�5bG1��kf����f�H�A+H�5BG1��Kf���f�I�\$H�{ ������uxH�{ �ux���PA�U�PA�U�PA�U�A�E�[A\A]A^]��H��H�5�F1���e���E���fDH��,H�5�F1��e���%���L��,H�
�F��1�H�5��%���D��UH��ATI��SH���:���H��tEH��H�H��tH90tH���݅����t)H�5�H��誔����t6L��H��[A\]釉���H�D�H�5�E1��e��[1�A\]�@H�Q,H�5�E1��d��[1�A\]�@��UH����1�]H��1��fb��fD��H�]�)H��tH�Q�)��UH��SH�<�)H��H���l����uH�%�)H�]����軪��H��H���p~��H��)H�]�����UH�����1�]H��1���a��fD��UH��AUATI��SH��H�����H����H��H�H��tH;0tH��胄������M�����]f��H��I�$H��tH;0tL���T�������L;ch���]���L��I��H����d��L���`��E1�E1�H��H�����H�5�L���Hx��L���`��H�{hH��t8�DH�{hH��tgH��1�L�
j���E1�S1ɾ�o��H�{h�`��XZL�chH�5��)H�e�H��[A\A]]�B}��f�H��$H�e�H�5�G1�[A\A]]�c���H�e�[A\A]]�DH�y*�����UH��AUATI��SH��H�����H����H��H�H��tH;0tH���#�������M��tf�e��H��I�$H��tH;0tL���������L;cp������L��I��H���c��L���_��L���C_��H�{pH��t��H�{pH��tG�r_��L�cpH�5w�)H��H��[A\A]]�%|��DH��#H��H�5^F1�[A\A]]��a���H��[A\A]]�DH��)�����U�F�H��AUSH��H����
wI��H�X@Hc�H�>��f�H��u�H�M�H�8�i��H�M�H��H�H�8�h��SL�
��1�PH�M�A��1�H���q�u�H�
5�V��Vh��H�� H�e�[A]]�f�H�P�G^��L�����H�5X�H�CPH�e�H��[A]]�4���@H�X�^��L��迊��H�57�H�CX��f�L���xn��H�e�H��H��[A]]�u���DL���Xn��H�e�H��H��[A]]���DH��]��L���f��H�5�H�C�k����H� �]��L����e��H�5<�H�C �C����H�(�g]��L���e��H�5�H�C(�����H�0�?]��L���e��H�C0H����H�5�H���+���H�5,�)H�e�H��[A]]��y��@H�8�\��L��蟉��H�C8H����H�5�H������H�5�)�f.�L���8r��H�5���C@�u���f�L���r��H�5i�H�߉CD���H�5��)�f���f.����n���H�C0�0���f.���n���H�C8�X���f.���UH��SH��H���G|��uIH�{`tH������H�{`u:H��1����H��1�����H�(�)H��H�]�H�@(��f.��{����L���H�
kD�&1�H�5q��������H��)H��tH��)��UH��SH�̚)H��H���e����uH���)H�]�����+���H��H���x��H���)H�]�����H�m�)H��tH�a�)��UH��SH�L�)H��H���He����uH�5�)H�]�����۩��H��H���w��H��)H�]�����H��)H��tH�ٙ)��UH��SH�ę)H��H����d����uH���)H�]����蛭��H��H���0w��H���)H�]�����H�U�)H��tH�I�)��UH��SH�4�)H��H���hd����uH��)H�]�������H��H���v��H���)H�]�����UH�����1�]H��1��&Z��fD��UH��AUL�-��ATA��L��SH��H���ڌ��E��tH��tPH�e�[A\A]]�DH��t�H��1�L�
β��E1�j1ɾH���h��XZ1�H�e�L��H��[A\A]]����H�����E1�E1�1�H�5&�H���p�����ff.���U�hH��SH���q���f��q��=:�0t
=��0u6���S�H�]��Hc‰�Hi��$I���H�� ��)ȍ�)���)��H��#�1�1��b��붐��UH��AUATI��H�={�SH����u��H��H��th1�H�ƿ ��g��H��H����]���L��I����_��1�H��tL)�H�P1�L����{��L��I���]��H��tH����e��H��L��[A\A]]�1��]���L��I���_��H��u�L��1�1��x{��L��I���m]��H��L��[A\A]]�ff.�@�������邆��f���UH��AUI��ATI��H��SH��H���`��H��M��H��L��[H��A\E1�A]1�]�~i��ff.���UH��ATI��SH��H�U�H�u�H��H�� dH�%(H�E�1��r����t`H�u�H�}�1��ɉ����H�E��tH�U�dH+%(u<H�� [A\]��H����V���Z��M��1�H�߉�H�
�!1��k���1���Os��ff.�@��UH��AUI��ATI��SH��H��葃��H��t\H��H�H��tH90tH���y����t@����H��M��t[I�$H��tH90tL���y����tAH��L��L��[A\A]]�d���@H���H�5�81���X��H��1�[A\A]]�fDH���H�5�81��X��H��1�[A\A]]�fD��1�@����u���ĉ������U�։�D��E��H��H���ED�Mj�5ۏ)P�%Z������U1�H�=rh��H����H�� 1�1��@]�D_��@���'`�����U1�H��AUM��ATI��SH��H��H��H�=��H����c��H��M��M��H��H��[�A\H�=�A]]���ff.����������U1�H��AUM��ATI��SH��H��H��H�=O�H���bc��H��M��M��H��H��[�A\H�=��A]]鋲��ff.���駅�����U1�H��AUI��ATI��SH��H��H�=j�H���b��H��M��M��H��H��[1�A\H�=I�A]]�!�������G������H�5-���kv��ff.���UH��SH��H��(dH�%(H�E�1����H��1�H��t�Mu��H�M�H�U�H��H��L�E��fZ��H�U�dH+%(uH�]����p��ff.����H�E�)H��tH�9�)��UH��SH�$�)H��H���]����uH�
�)H�]�����[���H��H����o��H��)H�]�����H���)H��tH���)��UH��SH���)H��H���]����uH�m�)H�]����蛸��H��H���po��H�I�)H�]�����UH��AWAVAUATI��SH��H��XdH�%(H�E�1��a���M����H��I�$H��tH;0tL���u�������m��H��H���fH�H��tH90tH���au�����ILc=Ґ)L�m�L�u�L��M�I�w�d���f�H�E�H9�(1�L��L����k����u�H��躅��H��H�E�����0I���q~��H��E1�E1�I��H�H�E�L��H�D���H�5�I�F�i��H��E1�E1�I�FL��H����H�5��H����h��H�}�E1�L��I�FA�H�����H���h��E1�E1�L��I�F H��H��L���h��I�L��L��I�F(��a��I�?L���JZ��L���B���H�E�dH+%(uJH��XL��[A\A]A^A_]ÐH���H�531��{S��E1���fDH��H�5�21��[S�����$m��@��UH��AWAVAUATI��SH��H��XdH�%(H�E�1��Q���M����H��I�$H��tH;0tL���s�������k��H��H���&H�H��tH90tH���Qs�����	HcŽ)L�m�L�}�L��L�u�L�H�pH�E���b���
�H�E�H9twL��L��L����i����u�H�E�dH+%(�WH��X[A\A]A^A_]�H�E�dH+%(�2H��H��XH�5�11�[A\A]A^A_]�*R��f.�H�pL�m�H��usH�pH����H�p H����H�p(H����H�]�L��H�{�lu��H�;L���!~��L���Y����?���@H�E�dH+%(��H���[����H�@H��H�E��Lg��H�E�H�pH���u���H�@H��H�E��'g��H�E�H�p H���]���H�@ H�xH�E��g��H�E�H�p(H���D���H�@(L����f���/�����j��fD��UH��SH��H��Hc��)H�DH��H���ig��H��tH�0H��H�]�����L�{�H�
�0�1�H�5X�C}����UH��AUATSH��H��HdH�%(H�E�1����H���H��H�H��tH;0tH����p����tcHc\�)L�e�L�m�L��H�t1��`���H�E�H��H�0�j��H��1�L��L���qg����u�H�E�dH+%(u'H��HH��[A\A]]�H�qH�5/1�1���O�����i��f���H���)H��tH�y�)��UH��SH�d�)H��H���(W����uH�M�)H�]�����;r��H��H���i��H�)�)H�]�����H���H��t��UH��SH��H���g���1�H��1���L��H���H�]���ff.���UH��SH��H� �e��H��H��tH���K��H��H�]����UH��ATSH���=��H��txH��H�H��tH;0tH���@o����t\H�{0E1��pm��H��H��u�2fDH�[H��t#H�;�j����u�H�3L����h��H�[I��H��u�[L��A\]�(L���H���H�5r51��KN��[1�A\]�@��UH��SH��H�����H��t&H��H�H��tH;0tH���n����t
H�CPH�]���H���H�5�41���M��H�]�H����ff.���UH��SH��H�����H��t&H��H�H��tH;0tH���n����t
H�CXH�]���H�/�H�541��{M��1������UH��ATI��SH�����H��teH��H�H��tH;0tH���m����tIH�{XL���=����u	[A\]�@H�{X�GJ��L���OZ��H��H�5�H�CX[A\]�8m���[H���A\1�H�5=3]��L�����UH��AVAUATI��SH�����H���}H��H�H��tH;0tH���m�����]H�{pH�C�t[H�5w3A\1�A]A^]�iL��f�L���8J��H��I���J��H��I����o��L�cpL���dL�k0L��H�Cx�_��L��H�C8�J��L��H�C(�o��L��H�C �r��L��H�C@�L��H�C��f������H��H�=;[��L�%��/T��L����{��H����v��H�{E1�E1�H��H��Z��H�5}��_`��H�{E1�E1�H��H��Z��H�5k��?`��1�H�ڿH�5�����L��H�{E1�E1�H��H���H�5���	`��1�H�ڿH�5x���L���^���L��H��H�5*�H������@���L��H��H�5�
H������"���L��H��H�5�H������H�{HtH�{0E1�H��H�AZ��H�5���E|��H��软��H���%n��H����Y��H��E1�E1�H��Y��H�5��H��I���;_��H�{�x��H�{H����Q��L��H���wl��H��1�[��A\H�5��A]1�A^]�*g��f.�H������@L���s��H����h��H�CH�"������H���H��t��UH��SH��H�����1�H��1��[G��H���H�]���ff.���H���H��t��UH��SH��H��藔1�H��1��G��H���H�]���ff.���H���H��t��UH��SH��H���W��1�H��1��F��H���H�]���ff.�UH��AWAVAUATSH��H�$H��H�EI��H�}H���H��H���H��L����L����H����H����dH�%(H�E�1��aS�����Jj�A�(L��j�L��p�H�� �A�L���k�ZY����H����H�����S������L���.aHcȉ��H9��r1�H��H����H���A���pH���H����@��<��g�ȺDž���A�ĉ׉��E1����Ti��Hc��I�ƅ������@E��~���1҅�~s����1�� fDH���L����莂��9��t4��D��L������D��u�H���L�����Z���9��ű����A9�u�H����H������Q���������A��A9��N���Hc��H����1�L��H�H���������,q��H��M��twL���LK���mH������A������H�V*H���H���tzH����x�������uD��F��H����M��1҉�H�
&1��/u��1�H�E�dH+%(u[H�e�H��[A\A]A^A_]�<�tH��)H����&���1��E���H��)H����ȺDž���A�������_��ff.���UH��AVE1�AUI��ATI��SH��H�� dH�%(H�E�1�H�E��T����t1L���B��H�E�dH+%(��H�e�[A\A]A^]�f�I�EL�M�L�E�H�PH�pH�H L�PH�8H�E�H9�PHB�L��AT����H��I��� T��ZY��u�H�u�H��tH���yF���s���@��T��H��H��R��L�pH��H�E�H�F�Eĉ�sL��1��A���>����^��f���UH��SH��Hch�)H�H�_���H�H�]������UH��AWI��AVAUA��ATM��SH��H����P���L��X���dH�%(H�E�1��{��M���H��I�H��tH;0tL���d��������e��H��H����H�H��tH90tH���|d������M�wI�>����H���T��I�~u	H����M��t2�&D��H��L��L��A��H��H�
���1�PA�1��_��^_H�E�dH+%(��H�e�[A\A]A^A_]��H�E�dH+%(�[H���H�e�H�5�$1�[A\A]A^A_]�:C��f.�H�E�dH+%(�H���@H��X���L��1�L���o��H�5e���I��H���jG��L���N��H����H����?��E�n8M�oI�FL�u�I�}���H���S��H��H����s��H��l���H��h���H��H��X����i��L��H���t��A�M8����foU�H��X���1�AU(�ic��I�E H���4�b��I�E��P���ub�5�~)L��1�L��1��UO��L��1�L��H������n��H�5����H��H����k��H���G?���Y���f�L��L��H���G���l���DH����T�������Ao](I�E )]�H��P������H���֦��H��H��@����7O��H��H���|l��I��H���F���H�}��7Z��1�H�u�H��H��X����V���,M�H��X����,u��ʉ�H�����L����X����urH��X�����H��X����N������sA��H��L��E1�A��H�W�1�L��PH�5�~��1��k\��XZ�K���@H�5�	L���J����@H��t���H��p���H���bC��L���zR��L����Y����4������lj�0�����Hc��P��L��ھ�H��H��8����[n����0���1�A�؋�4���H��8����>_��H��P���H�U�H��x���I���V����f.�x���zu	f.E�ztafn�0���fn�H���H�u�fn�4���fn�L���H��@���fb�fb�fl�H��)E��F��H�߉���E��f��L���Z�f(��vA��H��P����D��f�f��L��H�Ë�L�����H���+�p���+�t���H��+E�+U��*��*��n��H���h��H���b��L���v[��H��8����z<��H��X���H������H���L����DH��X����\>��I�} f���Z�f(���@���:�����X��ff.�f���UH��SH��H�����H������H�s@H��u"H���r���H�+{)H��H�]�H�@(��DH�C@H���XT����fD��UH��ATSH���]��H��襣��H��1ɾSH��E1�E1�1��J��L�cY^I�|$H��tDH��1ҾE1�SL�
�1��J��L�cXZI�|$I�t$H��t�K��I�D$L�cI�<$I�$H��t�?;��H�CH�xH�@H��tH�e�[A\]�g��DH�e�[A\]�ff.�@��UH��AWE��AVAUATSD��H��XL�U8L�]@H�u�H�EHL�eH�U��M�L�m L�U�L�u0L�]�H�}�H�E�dH�%(H�E�1��.���E�H���O���L�]�H���u�L�U�I��H�E�L��M���M�H�U�ASH�u�H�}�ARAVP�E�u(AUATPSAW��b��H��`��t�E�H�U�dH+%(uH�e�[A\A]A^A_]��V��ff.���UH��u�uAQA�����APA�����jjjj��������UE1�H��APE1�j��������UH��E�ujjjjjjP�����ff.���UH��ATI��SH�u�H��1�H��H�(dH�%(H�E�1��-�f��*E��A$�H�E�dH+%(u	H��[A\]��U��ff.�f���UH��ATI��1�SH��H�U�H��H�(dH�%(H�E�1�轠f��*E��A$�H�E�dH+%(u	H��[A\]��=U��ff.�f���UH��ATI��SH��H�� dH�%(H�E�H��w)���L���:���$R��L���E��I���R���M�H�{(�,��,�蜣H�U�H�u�H���\e��f�H�{(�
t�ZE��X��,�f��ZE��X��,���H�E�dH+%(u	H�� [A\]��lT��ff.����UH��AVAUI��ATI��SH���sc��L��H���D��H�{(H��t
I���g�L9�t[A\A]A^]�f�L���hk��H��I��H���M��L���"7��E1�H��L��H�C0A�H�����H�5���N��1�L��H�C8�N���L���Q��H�s@H��t�H�C@L��[A\A]A^]�O����UH��AUATI��SH���בM����H��I�$H��tH;0tL���Z����������1�H��1��6��H���D
��H��茞��I��H����L�c(L����H�{(H�SX1�H�CP�a�H�{(�8�H��L��E1ɉCHE1�H�����H�5����M��H��p)H��L��H�C@A�H�5y��sj��H��H��[A\A]]�DH�g�H�5B1�1��8��H��H��[A\A]]�H��H�5�1�1��8����ff.�@��H�(�Ý��H�FH�8tBUH��SH��H���1a��H���9=��H��H�HH�PH�CH�]�L�FH�8��y�f��ff.�@��UH��ATSL�gH��I�<$H��t
[A\]�@H�
�G��1�1��D��I�D$���H�����H���`��L�cH��蒊H��E1�E1�I�$H�CH��l��H�5��H�8�KL��H�CH��E1�E1�H��Q��H�5x�H�8�(L��H�C[A\]H�8�h����UH��AWAVAUATI��SH��H�����H���X���L�sI��I�~I9�t%M�~H��tL���D��M�fM��tL��L���4��L����_��H��tH������A�H��L��H�����H�5���Oh��A�H��H�ϋ��H�5��L���0h��E1�H��L��H� ���H�5W��h��H�CH�8t:L���R_��H���Z;��H��H�HH�PH�CL�FH�8H��[A\A]A^A_]鑌�H��[A\A]A^A_]Ð��U1�1�L�
�E1�H��AUATI��SH��H��V��`B��H�CL��H�x�pL��L��I���%���I�EH���b��H�CL��H�p虞XI�MH��Z�5r)H�e�1�[1�A\A]]��B�����UH��SH��H���{�H��H�]��H���k�ff.���UH��SH��H���^��H�,q)�q)����H�]��H�C H�T���H�C0��A��H���H�5�H��H�=���xd��H�߾H��p)H��p)�=E����=��H����SE1�E1�jH��1ɺjH�=��1�j�D��H�� �ip)���H��E1�E1�jH��1ɺjH�=��1�j�TD��H�]�H�� �2p)��@H�59p)H���5������ff.����UH��AUATI��SH�����H���_���H���E��H��H��u#�f.�H���=��H��H���|H���E����u�H��L�����I��H��t]H��L����L��H������I�|$I9�t)H��t�1��M�l$L���0��H�5do)L���M��H��L��[A\A]]��0��f�I�|$H��t.�0��H�52o)L��I�D$H��[A\A]]�oM���H��[A\A]]�ff.�f�UH��H��AWAVAUI��ATI��SH��(fDH��H���<��H��u�H���N����tH���<O����tI�} H���\I��I��H���XH���O��������#��H��I���Z��H����H��H�=��1��A��H��I���B��L��H��H�E��\ ��H��H����M����H��H�E���L��H����E��H�M�����H����O��L��H��H�E��
 ��H��H��t#H��H�U����L��H���E��H�U����YH�u�L�����H��H��t#H��H�M���L��H���wE��H�Mȅ��PH�u�L�����I��H���+L���[�L��H���@E�����L���.��L��I���L���mc��DL���p.��I�} L��L���a?��L��L��E1�E1�H��H�5v��F��L��L��E1�E1�H�L��H�5h���E��L��L��E1�E1�H��H�5W���E��L��L��E1�H�E1�H�5S��E��L��L���#���5�l)L��1�H��(1�[A\A]A^A_]��=��f.�H��H�E��A��L��H��H�E��u��H�U�H��I����L��H�U��y-��H�U�I��H���-��M����H���W��H��H��t�\�I��H������H���:��H��H��t�;�I��H�������H���gL����u"H���/���ƅ�~L��轠I��H���y���H���F��H�E�H��tuL���8�I��H��teM�>L����]��H�u�H���)R����uAL���-��H��H��t1�7��I��� ��L��H���m��L��H�E��,��H�E�H���DM�vM��u�H���B����unH����_��H��H��t^�^��I��H���SH��L�:H�U�L���;J��H�Uȅ�u I�} L��H�U��E��H�U�H��I���~���H�RH��u�L����`��f�H���#��I��H���l���H��([A\A]A^A_]�DH���pL��L��H��H�E����H��H��tLH���+��L��I���$���L��I������I��L���+������L��H��H�U��l+��H�U�I�����L��H�u�L��H�U��M��H�U�H��H��t*H��t�I���F���@H���(+��I��H����������H�u�L��H�U����H�U�H��I���l���H��uE1��R���1���_�����I���#���H���~K��L��H��H�E����H��H���S���M����������D��H���d���@��UH��AUATI��SH���W���H��蟒��H����L��H�
�d)H�5�d)1�H�=�d)H���(:��E1�E1�L��I�D$ H�#H�5v�H����A�����H���G���H���\��H��t&I��H��@H�3L������H�[H��u�L����8�����H������L��E1�H��H��H�53�H���H^��L��H��E1�H����H�5#��,^���w���H��L��E1�[H��A\E1�A]H��H�5��]�.A��ff.�UH��AUATI��SH��H��H� ��B��H����H��I���0)��H�{ L���O��L��L���I!��H��L��E1�SL�
�1�1Ҿ�Y8��H�$L��E1�L�
�F��1�1Ҿ�:8��H�$L��E1�L�
�1�1Ҿ�8��H�$L��E1�L�
�1�1Ҿ�7���5Rg)H��1�1��8��XZH�e�[L��A\A]]��(��fDH�e�[A\A]]�D��UH��H��ATI��H��SH�����H��L���	���H��[A\]����ff.�f���UH��H��ATI��H��SH�����H��L������H��[A\]���ff.�f���H��H��H���n���ff.���UH��AUATI��SH��H�~ �6��H��tFI��H���H�3L���%���H�[H��u�I�|$ �R����u/H��L��[A\A]]�l6��@I�|$ �R����uH��[A\A]]�L�X�H�
)	��1�H�5���V��ff.���UH��H��AVAUI��ATSH���C(��H��t6H����1��I�����L��H�����L��I���&��M��tH��L���("���5je)H��L��[1�A\1�A]A^]��6��f.����H�}h)�`0f����ff.���I��E1Ƀ�td����Hi�%I�$H�� )�����D�N�� tJA�y����
�D�<6����Hc�D�V�I�8I�|8�L)�H��f��
H��)�H9�u�A�D����������G1҄�t8Gt	���fD8Gu�8Gu��w�@9�u�����9�}��W9�����f����*L�BuCUH��SH��H��M��tH�J H�rH�z1�A��H�{�%��H�{�%��H��H�]���r%��f��ff.�@���ff.���Hc�e)H�D88�����ff.���H����@��UH��SH��H���;Q����e)��u	H�]���H��H�]�H�5�e)���(��@��UH��SH��H���P��H��e)��e)��uJH��H��#H�31�H�C0E1�E1��jH�=��1�jj�'7��H�]�H�� ��e)���H�5ue)H���q(���ff.�@��UH��AVAUATSH���iP��H��e)��e)����H�]���H�C0��?��H�3E1�E1�P1ɺH�=V�j1�jj�6��H�� �ee)�?��H�3E1�E1�P1ɺH�= �j1�jj�[6��H�� �5e)�?��H�3E1�E1�P1ɺH�=��j1�jj�'6��H�� �e)�N��I���N��I���X:��I���@?��H��H�3E1�AVE1�1ɺAUH�=��ATP1�jjj��5��H��@��d)�?��H�3E1�E1�P1ɺH�=y�j1�jj�5��H�� ��d)��>��H�3E1�E1�P1ɺH�=L�j1�jj�i5��H�� H�3E1ɉMd)E1�1ɺjH�=�1�jjjjj�55��H��(H�3E1ɉd)E1�1ɺjH�=��1�jj�5��H�� ��c)�8>��H�3E1�E1�P1ɺH�=��j1�jj��4��H�� ��c)��L��I���|=��H�3jE1�ATE1�1ɺPH�=��1�jjj�4��H��(H�3E1ɉ�c)E1�1ɺjH�=�1�jj�e4��H�� �cc)�VL��I���=��H�3ATE1�jE1�1ɺPH�=S�1�jjj�%4��H��0�'c)��2��H�3E1�1�PL��[)�1�jH�=(�jj��3��H��H�3E1ɉ�b)E1�1ɺj1�H�=�jj�3��H�� ��b)�p<��I���T��H�3ATE1�jE1�1ɺPH�=��1�j�3��H�� ��b)�4<��I���=��H�3ATE1�jE1�1ɺPH�=��1�j�G3��H�� �Yb)H�e�[A\A]A^]�H�5Ib)H���$���X���ff.��UH�=w�H����:��H��L�
����A� jH�ƺ��PH�
���#����f�UH�=?�H���:��H��L�
�rA�HjH�ƺ��PH�
����T#����f�UH�=
�H���`:��H��L�
�&A�hjH�ƺ��PH�
I|�#����f���UH��ATSH����K���
�`)H��`)����H�Q�H��E1�A����H���H���H�
_H�CHH��fHn�1�fHn�L�%�H�=m�fl�L��L��Ch�@��I��L��L���@�
�����f��H�==�H�=`)��J��H�߾H�1`)XZH�e�H�`)[A\]��1��H�5`)H����"���,���@������UH��AUATS��H����t_H�I��H�8��(��I��I�$H�8��(��AUL�
ȩ1�PA��1�H�
��A�t$H����S�0(��H�� H�e�[A\A]]�G0H�e�H��[A\A]]��5��f��w(H���=��D��UH��AUI��ATSH��������u,H���$��fA.E0����H�e�[A\A]]��H�I��H�8�(��I��I�$H�8��'��AUL�
�1�PA��1�H�
��A�t$H����S�Q'��H�� H�e�[A\A]]�f�H���X"��A9E(t�A�E(H�5^)H�e�L��[A\A]]�:��DH�5i^)�AE0�ڐUH�=`�H��SH���{7��H���;��H��H��L�
E�jH��A�X��H�
����) ��H�]�����wTI�ȉ���A��H��H��uB�~���t:A�I��@����D���������Hc�������A2A���A����u��UH�
o��H�5��H�=��H���4,��@�G���'wN�G�ǃ���������t5����Hi�%I�$H�� )��������4��t����ȃ�7)��H�O��UH�
��QH�5G�H�=�H���+���UHc�H�����G�����'���G�ǃ����Ƹ��t=����Hi�%I�$H�� )���������<��|�������7)���~A�p��$H��H�4�Hc�]H�<�H�
ZH�5
H�H�����)������pH���H�
��QH�5i�H�=����*��H�
��FH�5J�H�=V��*��ff.�f���H��w%9�s!9�s�0�ȃ�����H������UH�
�
��H�5�H�=b�H���Z*��f.��A�ȍH��w79�s39�s/�0�ȃ�����H�HǸ���	��"E��Eˆ�UH�
 
�H�5u�H�=��H����)��DU�F�H��'���F�����H��H��$��w_H�
���Hc�H�>���H�H��]���f�H�H��]���f�H�H��]���f�H�H�g]���f�1�]�H�
���H�5�H�=��=)��H�
v��H�5��H�=s��)��ff.�UH��AWAVD�4�AUE�nD��ATE1�A��SH��1���H��X�}�H�����dH�%(H�E�1�Hc��%��D�+D���H���V���A��E9��E1�D���H���5���A��E9��E1�E1�fDD��D��H���
���A��A��	u�A��A��	u�D�u�E�f	E�~E1�E���D���D��H������A��E9�u�A��A��	u�D�u�E1�D��D��H�����A��A��	u�A��E9�u�L�]��}�L���;����Hc�1�D�x�E��E����1�E���H��H9�����	�t�H����A9�u����E�$E�4H�U��}�A�D$�H�M�E�n�A���E�A��H�u�L�]�D�E�D�}�E��D�m�@D��D��H������A��E9�u�A��E9�u�H�U�H�u��}�H�M�H��L�]�D�}�D�E�H9��P���H��E��E��H9��!���E��}�wH�E�dH+%(��H��X[A\A]A^A_]�A9������D��	�����H��E��E��H9������@H��H9�����H��E��E��H9�������fDE�nE1�A��	E��f.�D��D��H������A��E9�u�A��A��u�E1��D��D��H�����A��A��u�A��E9�u������2����UH��ATSH���
B��H�W)��V)����H��H�z
H�C0H�?fHn�H�
�fHn�H�fl�fHn���fHn�fl����B<��I���Z5��H�3jE1�ATE1�1�1�j@H�=�P1�j@jjj��'��H��@H�3E1ɉPV)E1�1�1�j@H�=Ƽ1�jjj�'��H�� �-V)H�e�[A\]�H�5!V)H������$���ff.����UH��AWAVL�}�AUL�m�ATSH��H��XL�wdH�%(H�E�1�L�u��4��H�%����1��)��I�6L��L�u�I���'���H�E�L��1�H��H�pL�@@�P8L��L��L���s.����u�H�E�H�8��@��L�����H�dU)H��P0H�E�dH+%(uH��X[A\A]A^A_]��0��ff.���UH��AWI��AVM��AUI��ATI���(SH���5&��L���H�����L��H�C���L�{H��L��H�CL��H��L�s H��[A\A]A^A_]�	��f���UH��AWI��AVI���(AUM��ATI��SH���%��L���H�����L��H�C���L�{H�CL�k ��3��L��H���E��H��tHH�����I��H��tYH��H��H�Y1�jL�ƺ1�H�=�,)�)��XZH�e�[A\A]A^A_]�L�}�H�
�T1�H�5n��B��L���H�
��V1�H�5M���A��ff.�f�UH�=`�H��SH����,��H��L�
�A�jH�ƺ��PH�
������H��H���?1����R)H��H�]���UH�=	�H��SH���,��H�����H��H��L�
EjH��A� ��H�
���9���H��H����0����R)H��H�]���f.���UH�
DH��SH��Hc�R)H��K)H�5�K)H�H�_H�=�K)�$!��H�H�]���ff.���UH��SH��H��H�?����H�;�`��H�{�W��H�{�N��H�{ �%��H�{(��,��H�{HH�CHH��t�vC��H�{P�}��H��H�]������UH��AUATSH��H��L�o�g��H�Ӹ���1��B%��L�C@H�sL��I��H��1�S8H�K�5�Q)L��1�1�� ��I�EH�sH�8��6��H��L��[A\A]]����@��U1�H��ATSL�gH�=W��"��I�<$H��H���)��H��I���9��M��t[L��A\]�8����[A\]�ff.���UH��SH��dH�%(H�E�1��E���uH�E�dH+%(u)H�]���1�H�U�L���2����t�E�t�����K,��ff.���UH��SH��H��H�H�CH��t��.��H�;H�5�I)�+��H��H�]���^��ff.���U1�H��AWAVAUI��H��x���ATSH��dH�%(H�E�1�HDžx���HDžp������I���*��H��x�������&�����?��I�EL�xH��x���L��P���H����L�`H��p����>��H�
O�1�H�߉�M��1��F@��H��x�������I�uI�}1�M�E@H��p���A�U8I�?I�u��4��H���
4��H�E�dH+%(�H�e�[A\A]A^A_]�H��@���H�5���h���H��H��`����e$��M���}��h���L��H���M��H������L��@���L��������\�����M�M����M�'L�����I��H��t�L��L��������&��H��H��H��h���� ��� DH������H�5���]B�����L��L��H���'����u�H��h����,��L����7��M�M���w���L��H�����\���H�5�G)L��@���L�牕h����Y)��H��`����-��H���#��M�e H�52�H��L���/2����t��h�������A�E0��I�}1�H��L���;��H������I�MI�}1�I�EHA�E0M�M P�5�M)1�A�u(M�E���XZH���%���H���������D1�L����0��H�����H������H��`���H�5�H��1��=��Dž\�������H�=S�L���������L��H���b"��I�U H��1�H�56�L����<��M�}@L��M�e8�
��1�I�uI�}H��M��A��H��P���I�uH�8�$2��H��p����h1��H���R���H������E���f�H��x��������2���H�5�E)1��'��H��`������H���!��M�e H�5��H��L���0�����r����x�����'��D��UH��AWI��AVE��AUI��H�=!�ATSH��8H�EH�u�H��L�E�H��H�E�H�EH�E�dH�%(H�E�1����I��I�EL��H�8�'$��H��tH���������7��L��H���
��H�C�!��H�}�H��p
��L��H�C���H�}�H�C �7��D�s0L��H��H�C(H�E�L�cH�C8H�E�H�C@I�EH�8�)*��H�=�����H�{PH��� ��A��uuA����L�c��*��H��L���<��H��H����.��L��H�����I��H����H�E�L�5���L���E�H��L��H�E��=���}ą���1�����H�CHH�E�dH+%(���C0H�K1�H�{�5�J)�EH�C(H�EL�K 1�L�CH�e�[A\A]A^A_]�m��DH���H�5*�1��#��H�}�L�-�")���E1�M��L��H��H�
��H�5��1���!��H���1�SL�H��L�
����L��I���["��XZH�E�dH+%(��H�e�L��[A\A]A^A_]� (���+��H��I�EH��tH;0tL���,����t~��'��L��H���~;��H��H��tH�U�L���E��s<���uą�������#=��L��H���H;��H������H�U�L��H���E��9<���Mą�����s���f��{��H��I�EH��tH;0tL���b+����������u$���W����$��L���H�
����1�H�5���:7��f.���UH��H��ATI��1�SH���e��H��t H�B)[L��H��A\]�
��f.��
��I��L��[��A\H�
گ�1�]�:��ff.����UH��SH��H���G@H�0���H�{0H�C0H��t�J���5��1�H��H�iUH�C0H���6��H�5����H��H���K��H���c��H��t.H�5'WH���3��H��tH������H�]�1���f.�H�5+�H������ff.�@UH��AWI��AVI��1�AUI��L��ATM��SH��H�U�H��H��HdH�%(H�E�1�H�E��E��1)���}�t+H�E�dH+%(��H�e�[A\A]A^A_]�f�L���'��I��H����I�}1�L�}�M��LE�H���
L�E�H�}��&��H�}�H���8��H��H���H�E�����H�}�H�E��
��H�}�L�E�H����H���H���#M��tL��H��H�=j�1�L�E��6��L�E�I�jH�U�H�
��1�SH�50�H�=�)L�
h����YH�=�)^H��H��taL�}�A�EM��M��H�^��AUH��H��PMD�E1�L���29��H���J$��L������XZ���H���H�5j�1����L�ѭH�
u���1�H�5r��4��f�H�B�H�52�1��k����f�H�H�H�5�1��K���f�H�>�H�5��1��+����� ��L�٬H�
����1�H�5��3����H����UH��AWAVAUATSH��H���>uH��[A\A]A^A_]�I��I�����I�|$H���6��I��H��tiH����	��I��H��tzI�|$�n.��H��H����L��L��H�=Ŭ1��^��L��H��L��L��I��I�����H��L��[A\A]A^A_]�q����L�s�H�
����1�H�5��2��L�Z�H�
����1�H�5��2��L���H�
����1�H�5̪�l2��ff.������u�fDUH��AVM��AUI��ATI��SH���t��H��M��tI�$H��tH;0t^L���&����uRH��t}H�;@t�@H�������tfH���N��H��tY�8tT[L��L��L��A\E1�A]H��A^]�����H�5��L���17����u�[L��H�5���L��A\A]A^]�4��[A\A]A^]����UH��H��AUI��ATI��H�5���SL��H��H���� �����u>H�CH��tH�K 1�L��L���H�{����H�{����H��H��[A\A]]���H��[A\A]]�D��UH��H��AUATH�u�SH��H��(L�bdH�%(H�E�1�H�E�H�E��]��H�E�H��t+L�h��	��H�,�1���L��1����H�}�H�E��y��H�K H�sH�U�H�{A��H�}��&��H�{�$��H�{���H������H�E�dH+%(uH��([A\A]]��t��@��UH��SH��H��H�H�CH��t�����{@��uSH�{ H�C H��t��,��H�{(H�C(H��t����H�{8H�5�:)���H�pA)H��H�]�H�@0��f���4���C@�f�UH��AWAVAUATSH��H�u�H���H���)I��H�}�1�1��y!��I��H����L���%��H��H��tbH��L����+����unH�}�1�H��1��.��I���6&��1�1�L��H��I�������uPM��tL���c+��L�����L���3%��H��H��u�H��L��[A\A]A^A_]���D1��i����g���@E1�1�H�ΨL��H�5ɨ��
��H��H�E�t�H���D��H�U�L��H������p���H��[A\A]A^A_]�L�N�H�
+��a1�H�5I��P.��L�V�H�
��b1�H�5(��/.��ff.�@UH��AWAVAUATSH��H����I��L�5)�E1��d0��1�L��H��1���	��L��H��H�E��6������H�8I��H��t9fD1�L��1��	��L��H��H������H���>���A�GI�|�I��H��u�H�}�H��[A\A]A^A_]����L�S�H�
 ���1�H�5N��U-��DUH��AWI��AVI��AUI��ATSH��8dH�%(H�E�1�Hcl>)L�$I�<$�S��H��H���82��H�E�I�T$`1�PH�E�E1�L��PH�E�H��A�PH�E�PH�E�PR���H��0H��A�����E����H�E�H��t}I;D$`uSH�}�H��vLH�}�M��tH�I�H�G��I��?���H�U�dH+%(u:H�e�[A\A]A^A_]�@H��H�=���1�����H�}����1���l��ff.����UH��SH��H����(��H�T=)�J=)���H�H�3E1�E1�H�
�fHn�H���fHn�H����H�C0fl�H��H�=ڛCH�CH1�jjj����H��H�3E1�jE1����jH�=ݥj��<)1����H��H�3E1�jE1����jH�=C�j�~<)1��s��H�� �q<)�t���H�=��A�H��H��H����-��H��H�]�H�+<)H�,<)�����f�H�5%<)H���������ff.����UH��AUI��ATSH����tgH�I�̉�H�8���I��I�$H�8���AUL�
~�A��PH�
�1�H�p�A�t$�H�=��S����H�� H�e�[A\A]]�f�H���X��I�EH�e�[A\A]]�f���UH��AVAUI��ATSLc5S;)��tjH�I�̉�H�8����I��I�$H�8����AUL�
مA��PH�
m�1�H�˨A�t$�H�=�S�<��H�� H�e�[A\A]A^]�H���
��K�D5H�e�[A\A]A^]�ff.�����twUH��AUATI��S��H��H�H�8�J��I��I�$H�8�;��AUL�
1�A��PH�
��1�H�#�A�t$�H�=K�S���H�� H�e�[A\A]]�DH�wH������@��UH��AUATI��SH��Hc
:)��thH���H�8���I��I�$H�8���AUL�
��A��PH�
.�1�H���A�t$�H�=��S���H�� H�e�[A\A]]�fDH�4H�e�H��[A\A]]�R��f���U1�H��SH��H��H�G81��-�������������H�C`1�f�C@�CBf�SD�CFf�KH�CJf�sL�CNH�]���@��UH��AUI��ATSH��Hc9)H�9)H��PHH�;����H�;1�L��H�5�I�����L��1�H�5F��CH�	��L��1�H�5:�H�CX�	��L��1�H�52�H�C`�{	��H�ChH��[A\A]]�ff.��UH��ATSH��H��pH�dH�%(H�E�1��Z��1�H�5�H��I���&	���K@H�s8E1�H��A� L������KAH�H�E������KBH�H�E������KDH�H�E������KEH�H�E������KFH�H�E������KHH�H�E������KIH�H�E������KJH�H�E������KLH�H�E������KMH�H�Eȉ����KNH�H�EЉ��ȹH�H�E�H�E�jP���XZH�E�dH+%(u	H�e�[A\]��7���UH��AVAUATSH��H��@H�dH�%(H�E�1����H�s H��I���w)��H;C8���sPH�{L�m�L�5�T�6����CPH�s8L��L�e�� ��H�C8H�s`L���	���9f�H��1�M��E1�SH�}�1ɾ���L���
��H�}���XZ1�L��L��������u�H�E�dH+%(u%H�e�[A\A]A^]�H�s 1�1�L�������>����&��fD��UH��SH��H��H�8t����H�{X�;��H�{`�"��H�36)H��H�]�H�@0��ff.�f�UH��SH��Hc�5)H�H�{u-f�{L�C@Cfo�C0��u[H�]���DH�;���H�sH��H��t$H�SH��tE1�1�H�E��=���H�}�f�H�s���H�C�D�CLH�]���)�����UH��ATI��SHc5)H�H�{PH�CPH��t�r��H�;H��t�sH��u3H�{uH�{tL�����H��4)[L��A\]H�@0��f�������f�UH��AUATL�m�SH��hdH�%(H�E�1�Hc�4)H�H�;�m��H�}��
H�E�I��1�H�E��H�L���E��~CfnS4fn[$fnK0fl�)E�fnC fb�fb�fl�)E��'��H�s1�1�L��M���I���L�����H�E�dH+%(uH��h[A\A]]�����f���UH��AWAVAUATI��SH��Hc�3)H�H�;���D�k8D�{<�S �K$H��I��E��DHk(E��DH{,H�sE��E���s��H�{u�CLH��1�[A\A]A^A_]�@L���8'��D;k0uD;{4t%H�sE��E��1�1�L���&���C@��u?D�k0D�{4�St��u@�C@��tf�L���X����k@u�L������y���D���C@��H�sL������Ct�ff.�UH��AUI��ATSH��xHc�2)dH�%(H�E�1�H�H�;����{LI�ą���L���Y&��H�sH�U�L��H�G�H��x���H�C(�g����t#H�E��t[�E����N‰C(�E���N‰C,�CpL���!��L��H�=7�������CLH�E�dH+%(uIH��x[A\A]]�D��t��E����N‰C(�Eą�N‰C,�fD�CL�%���6����
��f�UH��AWAVA��AUI��ATI��SH��Lc�1)dH�%(H�E�1�N�<I�?�a��M�of�H��H��)� ���)�0���)�@���)�P����"%��I�wH�ߺB���H���)�����t-I�GH�E�dH+%(��H�e�[A\A]A^A_]�H��L�������$��E����L��`���I�wL��L��L��A�GpA�GD������¸��t!H������H��HG�A�GD��`�����fn�H��fp��fA�Gt�t
��H���L$��I�w�1�A�H�����H���K
��Hc@0)IcGD�A~GM�4fHn�I�GI�>fl�)���H���������f��fo���L��`����t���H��I��M��d����~�����)E�ANXL���)M�f��)M�Dž`���!�E� �#��L���1�1�H������L�����L���	��L�������5n/)L��1�1�����I�wH������D����I�wL��H��������iH����H������H��H�� ���H�@H��(����\��I�GPH���/D�pH�x(x x0�kH����I�?D������Dž����A9�L��`�����H�����A�G|���AVA�H��H���H��hA������������R�����jPj���I�wH��0H��I�G�"��H�����I�H������H�����H���tI�WI�wE1�1�H���
�H���H������H������C���I�w1�H���N��H��I�wE1�jE1�1ɺjH��j��H�� �	���f�H���������I�wI�OH��H��L������L�����HDž����D�����tH���������@����I�?���I�G���e	��D��UH��AVI��AUATI��SH��HcL-)dH�%(H�E�1�H�H�;�&��H�SH��tUI�N I��H9�tH;KuCA�����w8H�5��Hc�H�>���I�v(1�L���:���H�{���H�E�dH+%(�GH��[A\A]A^]��H�sH��t�H9�u�I�F(H;Ch�H;C`u�1�H�M�L�������t�H�EЃ�9Cxt�H����L���? ��H�sL�����L���K��Hc@,)L�xx�_����@xH�E�dH+%(��H��L��[A\A]A^]�I���f�H�{����H�E�dH+%(uhI�v(H��L��1�[A\A]A^]����@H�CH����I9F(����Hc�+)L�Hx������H�u�H�PtH�E�dH+%(�a����i��f�H�CH����I;F(�����I;V0�w���L���g��55+)1�L��1����H�CL���E���H�E�dH+%(u�H��L��[A\A]A^]�r�f�I;V(����H�E�dH+%(�e����5�*)H��L��1�[1�A\A]A^]����DI�F(H9�uH�CH�SH9�����H�������L�����5*)1�L��1�����H�CL����H�E�dH+%(�F��������H�CH���k���I;F(�a���Hc.*)L�Px���L�������H�{����I�FX�ue��%���H�E�dH+%(�l���H��L��[A\A]A^]�&���fDI;V0��������I�v(1�L������H�C���f��C@H�E�dH+%(�l�������@�CpH�E�dH+%(�I���������H�
*)H��tH�*)��UH��SH��))H��H������uH��))H�]��������H��H��� ��H��))H�]�����A�ɉ�5�))A��1�1��%���D��H��3�����H�������H������H��S����H�������H�������H�������5B))1�1����ff.�f���H��5#))1�1��z���f.���A��H��5))I��1�1��T���@���5�()1�1��=���ff.�f���I��H��5�()A��1�1�����@��UH��H�� dH�%(H�E�1��M�H�M�D�E�H�E����H�E�dH+%(u���g�����H��53()1�1����f.���H��5()1�1����f.���H��5�')1�1��j���f.���UI��H��5�')1�H��H��APA��1��:���XZ��fD��H��5�')1�1�����f.���H��5�')1�1����f.���UH��1�H��H���5�')dH�%(H�E�1�L�E�����E�H�U�dH+%(u���H������5J')1�1����ff.�f���UH��1�H��H���5!')dH�%(H�E�1�L�E��Q���H�E�H�U�dH+%(u���������UH��1�H��H���5�&)dH�%(H�E�1�L�E�����H�E�H�U�dH+%(u��������UH��SH��H������1�H��1���H�XH�]���D��H��%)H��tH��%)��UH��SH��%)H��H�������uH��%)H�]�����K��H��H��� ��H��%)H�]�����UH��H��t��P��w!]�H�
\���H�5��H�=_����H�
=���H�5b�H�=2����DUH��AWAVAUATSH������H�?�Hc�I�Ժ
����	��f.����	i�71ȃ�u��
	�A��A��T���eE1�f�D��D��D���L��A����������A��u�D���L��A�	A�������D��L��������D��L��������@D��D��D����D)�L��A�������d��A��u�L���v���A��D�p����D��D����)�L��������'����u�E�w�fD��D��A���L������������u�H��A�W�L��[�A\A]A^A_]����H�
���H�5��H�=����H�
���H�5n�H�=[����UH��AWAVAUATSH���Uȃ��2I��H��H��E1�����E̅���A�}E1�E1�@�ǍH����A9���D9���A��D������Hc�A�L
�����Eȃ�wH���Hc�H�>��D��D��H��E��o��C�>D��D����H��A����1������D9u�taA�E�H�ǁ���3D9��*D9��!A��D������Hc�A�L
��s��A��E�D9u��
����A��E9����H��[A\A]A^A_]�@D��D��H�����D�羫���D��H��C�>D��A��H��!D�D��E�D)��H��у�1�������D9u�t�A�E�H�ǁ���_A9��VA9��MA��D������Hc�A�L
���a���A��E�D9u��3����+���D��D��H��E����D�񾫪��D��H���D��D��A��H��!�H��у�1����,��D9u�����A�E�H�ǁ����A9���A9���A��D������Hc�A�L
���f���A��E�D9u�������x���D��D��H���R��D��D����C�>i�����=UUUU��A��E�1�H�������D9u��2���A�E�H�ǁ���A9���A9���A��D������Hc�A�L
���n���A��E�D9u����������D��D��H�����D��D��H��AiΫ�����UUUU��A��E�1�������D9u������A�E�H�ǁ���XA9��OA9��FA��D������Hc�A�L
���r���A��E�D9u��,����$���@D��D��H�����D�ᾫ���D��H��D��A��H��!�<ID��)�H��D�E��у�1���� ��D9u�����A�E�H�ǁ����A9���A9���A��D������Hc�A�L
���b���A��E�D9u��t����l���@D��D��H���B�������D��D����D���H��D��H��!D�@D��D)����A��E�1�H�����a��D9u�����A�E�H�ǁ����A9���A9���A��D������Hc�A�L
���[���A��E�D9u���������DD�����E�D��D��H���y���1�8E�D����D��H��A�����E�D9u��j���A�E�H�ǁ��w@A9�};A9�}6A��D������Hc�A�L
��s�A��E�D9u�� �������H�
���H�5�H�=����H�
��\H�5��H�=/��r�f���UH��AWAVAUATSH��H�EH�u���T����UH��@���H�E H��x����M�D��P���H��`���dH�%(H�M�1�H��u	H�����E���@��D9���@����A��(����P����H������T����~D��h�����X�����T����}����H�}�A���\L��x���E1�E1퉅p���M��L��M���(fDIc�H�DI�I���WH��I��H9]�taE�oE�gA�����A�����A�?�u��I���Hcȃ����Mc�H��I9�|���h���9]��P
�E��=���M��D��p���I�����E��A��E9�|���X���D��A����A��A��u��E�L��`���1���L�����P����Hc��f�H�}��E���L��x���E1�A�}�����D�U�A������E�JE)�@D�ȉщ�)���A�������H���A4��s�A�}�u�A��D�U��'���A�}H�M�L����E���E�UE��~K�}�1����I�M������H�Hc���у��у�������у�����A4A9Uĉ}�I��I��L9u��%����E�9��D��T����}��Ƽ��D�$�A9��DD��L�m�)�H��`���L��9�O�H��1�莻���u�H��L��1��ރ��y����U������L��`���A�ӻ����D9���D��E�SD�O�D�Љ�D��)���B�������H���A<��s�A����E9�~:D���A��������}�D��E��A�������9�|�D��T���A��u�����D��)Ѓ���D�E��}�(�ƒ�T����� ˆ������Hc�T���D�e�H��Mc�H��H�&�H�B�<8H�W�H�B�8��h���A��D��É�H����ֺ����T���D��A���E���E��A��A��A�FAI�����)�A����)��O���8������H��L�U�1�H��H��X���L�Ѓ��E
��X����
��X�����	��X�����	H��X�����E1�D��A�D��H��H��0����D�H�E�H��x���H�@�}�D��M�Չ�p���DA�}�1�@A���A��A��A1�A������!�D1��s�A�EI��L9�t
A2EA�E���}���p���A�1�A���A��A��A1�E������!�D1�A��s�E�qD9�H���t
E����S���A����h������PH��`���Hc�H��M��H��@���H�� ���M��H�<D�����D��h���H�L�����I��D��HDžH���I��H��(���H��H���1ɉ�9؋�8�����D�H��X����������$
H��X�����t����&
E���Hc����Ic�L�gD����������I�7H�]�H��0���������H�L���L����H����H��p���L��L�����I��L��h���I��D��f.�H�U�H��h���E�4$D27���E��E1�H��H��p����@C�L�1����@����1�A������!�1��s�B0I��D9��I��M9�u�D����������A��1�L�����������L���L����H����D��8���A��A�A)�A9�AD�H��Lc�D�C�H9�u�H��(���H��H���H��H�H�� ���H�DD�H��D�L�H9�u�H��H���H��H���I�A9�����L�����M��L��`����]�L�U�L�������������L�����XI�E1����L�U��x�����D�$���L��0���A��D��h���L��`���L��8���E��A����A�EE��A���������p���E�p�D��H�����X���D�u�D��(���A����X���D��H���D)뀽p���AE݉�D��L��訷����u
D9�h����Z�E�A��A9�u�A��s�D��(�����X���E���]���E��D��h���L��8���L��0���E9��H��`���L�U����L�U����� ���D�`�~7M��L��`�����1ɾL���k�����1ɺL�����W���D9�|�M�ꋅ ���A�����H��`���E��������A��A�@�D����������D�e�A��L��h���I��A��L��X���DH�D����E����p���E��A��D���DA��A��A��tuA�U����H�D9�ALǃ���u�A�$D��Љ��A9��;���3��p�����9�A��A ��h9U�s��U�D��1�L��A��A���c���A��u�A��L��D�e�L��h���A��L��X���A��A�������}�L��L�U����L�U�����~a�Z�L��`���Hc�1���p���L����E1�I�܉�D	���H����E����9�p�����I��I9�u�H��L��H9�u�M��E���������i�%1ȃ�u�I��H�M��I	�L��H���I	D�� ���1�A��E��E��H��`���D���A��L�U�I�D���J���D��D���H��`���A���2��������L�U�A9�u�����u�H��@����}�L�U��H�����P����L�U��L��`�����P�����T���L�����Z�H��@�����L���9�H�E�dH+%(�������H��[A\A]A^A_]�D��H��@���D�������D��A��H����L��������j����n���A9��|���A��A��A��9��"E���1ɉ�D��L�牅H����,���A�$��H���A9���!Ȅ��1��������p���A9�����p���D	��&���C�<*L��h����������X���E�<�}�H��H���H��8���L��(���L��0���A�܋E�A�����B� D��1�C�4/��D	�L����A��膳��A��u�A��A��u�L��h�����X���H��H���H��8���L��0���L��(���I��I9��F���H��L��H9��.����o�����A9���!����1ɉ�D��L�牅8���D��H������A�$D��H�����8���A9։������A9������H�H��X���H�_H���H�D�H��H)����������1�A���J�#9�r�E�������Ic��;���ƅp���A�A�����H��`���ƅ�������������E1�H��fD�H���X�����������H����X��������̉�1���ƒ�I�49�r�L����E1��]�H��������E1�L�U�H��8���D��H���L��`�����H�����T���L����,�H��@�����L����L������h������/L�e�E1�E1��E�1�E1�E1��E��&A��A����A��I���C9�h���tj��H�}�D�����ϰ��D8�t�H�u�H��x��������D�m�E��uH�}��S�����I�F(��LE�H�}�D����A�胰��A�čC9�h���u�H�u�H��x�������D�m�E��tH�u�H��x�����b����E�H�}������I�F(��LE�A�GD9�tA�����I���%�����p���E1��E�E1�E1�1��E��-fDA��A����A��I��A�D$D9�p���tmA��H�}�D��D��貯��8�t�H�u�H��x�������D�m���uH�}��8�����I�F(��LE�H�}�D��D��A��g�����A�D$D9�p���u�H�u�H��x�����b���D�m���tH�u�H��x�����E����E�H�}��ȣ����I�F(��LE�A�GD9�p���tA����I��������p���L�e�L��X���1҅���A�փ�E1���p����f�A9�tsD��D��L��譮��D�}�A��D��D��L��A��蔮��A8�uϋ�p����u�L���~���A8�u���p���L��D���h���H��X�����A8�H�GHE�H��X���A9�u���p���9��g���E1�E1�E1��
�A��D��D��L������<A�GA��D9�u�A�FD9���A����HDžX���1ҋ�h���H��X�����Hc�Hk��H�H��H��HI�H�D�H�H���D���H�H�H��8���H9�}H��8�����H�����P�����H���H��@���L����J��؃���H�����������P�������H�
��H�5woH�=������f.�Mc�K�T�H���6������X����D�����X���1�f�t����H�
����H�5oH�=����H�
f���H�5�nH�=#��f��H�
G���H�5�nH�=�q�G��H�
0���H�5�nH�=u��(��H�
���H�5�nH�=��	��H�
���H�5wnH�=tq����H�
˷��H�5XnH�=������1��5�H�
Z��%H�5/nH�=�����H�
���cH�5nH�=����H�
L���H�5�mH�=vq�d����H�
x���H�5�mH�=���@��H�
)���H�5�mH�=�p�!��H�
���H�5�mH�=�p���H�
���H�5pmH�=�p����H�
���RH�5QmH�=�p����@��UH��A��D��E��H��H�� dL�%(L�M�E1ɋu�E�H=�wVL��I���EH��@��D�M�D�MRD��WV��E�H�E�H�}�H�����H�� H�U�dH+%(u��f��1������@��UA�����H��H��APA�(Q�j�������H��t�9�s9�r1��f�����UH�
���H�58lH�=oH��������UH��ATSH��tJ�7H��L�%;�@��u�,@�sH��@��tL���#��H��u�[1�A\]��[�A\]�H�
?��H�5�kH�=Io�'�����H��u�(D��0<	wH�����u���1��UH�
ų�)H�5ZkH�=�nH������f.���H�������tJ��t��tp��t[��u~H��uy��H��H�TFH�H���SH�BH���f.�H��H�TH���������H��H����DH�vH���fDH���fDH������UH�
P��TH�5�jH�=�jH�����D��UH��AUATSH��H��H��uH��u]H���wsH��H=�cH��u,E1�E1��H��D�kH�KD�cH��[A\A]]�fDH��A��A���Z��H����H�
.��`H�5�iH�=�m�V��H�
��dH�5�iH�=m�7�����UH��AVAUATSH�� dH�%(H�E�1�H���eI��H��I��H���4���E�H��H=��bH�4�H���������H�H�VH��H��I��H����7�M�H���'A�$�E�����1�E1�1�E1�A�������A�D$I�������HЀ�	��C����D�DHЃ�u�A�������D�ھ	E�J	D)�DD�ȉ�D��)���2������H���@<��s�A�D$I��A��
1ҾE1����}���@��tD�UЅ�~�tRH�M�H��D�������E�A9���H�E�H�]�foE�I�FAH�E�dH+%(�H�� L��[A\A]A^]�@H�BH����s#�u��tG��t@��1�f�T��5f���H�{H�H�D�H��H��H)�����1��H�A�$�E����}���H�
����H�5~gH�=�k���������D��H�
w��~H�5LgH�=:k���H�
ز��H�5-gH�=����H�
9��oH�5gH�=�j����,��H�
��tH�5�fH�=�j�]��ff.�f���UH��AWAVAUATSH��8H����H�}�H��I��I���`��I��H=���H��I�D@I��I�H=����M��L�]�D���6A����71�E1�L�M�E1�M�L��H�y�E���U�M��I��L�]�L�E��A�GI��������H���L��H����Ek�-H)�A��E�$A��u�A��������Mȸ�����
D�Y
)�fDD�ډ�D��)���0������Hc���A<��s�A�GI���E�E1�E1��p����E��M�L�M�M���U�L�]�L�E�E��sD9���A�L��E�CM�kA�SH��8[A\A]A^A_]�DI�AH�������������A�E�����1�fA�L��A�������A�������YA��A)���؉�D��)���A�2������H���A|��s�D���4�����I�}I�EI�D
�H��L��H)�����1��H�A�������H�
W���H�5LdH�=Oh������A�EA�D��1�����H�
���H�5
dH�=(h�}��H�
����H�5�cH�=ۈ�^��H�
׫��H�5�cH�=ag�?��H�
����H�5�cH�=�g� ����UH��AWAVI��AUE��ATI��SH��H��h�E�M�D�E��E�dH�%(H�E�1����H����I��B��H����������H�H��x��������uiH���������!L9�x���rf1�fD�A�H��I9�u��E�I���w=J��H=�-�E�D�}�L�e���L����s���H9�x���sZA�1�H�U�dH+%(��H�e�[A\A]A^A_]�f�H��E��1�1�AVATD�}�AWD�M�M��U��k��H�� �DH�}�L��H�����H�E��oE�H�E�)E�H��H�}�E��AVATD�}�AWD�M�M��U����H�� �Y���DL������H9�x����4���H�}�L��H������oM�H�E�)M�H�E���������UH��H���JI��I��H��H���H���?��H��?B�<1���I��A�
f�PI��
�
�D�ډ�D��)���N�����Hc���@<��s�A���	A��D�҉�D��)��������Hc���@<��sۺI�@L��I�A�P]�f�A��
�BA��A��A�ɀD�
A��D�ʉ�D��)��������Hc���@<��sۺ�@��D���������s��o���H�
.���H�5C`H�=_`���H�
���H�5$`H�=@`������H���(H��tH���(��UH��SH���(H��H�������uH�u�(H�]�����+���H��H�����H�Q�(H�]�����UH��AVI��AUI��ATI��SH���p���H����H��H�H��tH90tH���?������M����1�H��L��L���^�H�5����H��H������H���$��H��toL�����H���(H��H�����H�5����H���&�H��tH��[A\A]A^]鑷���[A\A]A^]��H��b[H�5Q�A\1�A]A^]����H�5�H���1���}���@H��b��ff.�@��UH��AVI��AUI��ATI��SH���P���H��tSH��H�H��tH90tH���#����t7H�CL��H�8���H��tC[H�xPA\L��L��H�5�^A]1�A^]�k�H�4b[H�5)�A\1�A]A^]�K���H�Bb��ff.�@��UH��AVI��AUI��ATI��SH�����H��tSH��H�H��tH90tH���s����t7H�CL��H�8�`��H��tCH�xH[L��L��A\H�XA]1�A^]黿��H��a[H�59�A\1�A]A^]雸��H��a��ff.�@��UH��AWAVA��AUATI��SH��H��dH�%(H�E�1�����H���BH��H�H��tH90tH��������"L�kL��I�}���I��H���&A���A���BH�xP�9���H��I�����H���I�HL��H�53^�r�I�H詵��I��A�G0�kH�=�\�?���H��H��@���H��H��8������I�W L��1�H��8���H�5�\�j�M�G@H��8���M�w8L��0���蟺��I�w1�H��L��0���H��A��I�}L�����H�E�dH+%(�UH�Ĩ[A\A]A^A_]��H�E�dH+%(�(H��_H�ĨH�58�1�[A\A]A^A_]�Ƕ���L��������k���H���1���1��I��1�H��I�wI��H��M�G@A�W8L�����H�E�dH+%(��I�}H�ĨL��[A\A]A^A_]����f.�I�践��I�w 1�L��H��H��8����?�H��8���1�1�H���<���H��8����P����P���賻��H������@���H�E�dH+%(uH��^���������UH��AUI��ATI��SH��H���1���M��t\H��I�$H��tH90tL�������t?�:��H��H��tZH�H��tH90tH��������tAH��L��H��[A\A]]���@H�^H�5"�1��+���H��1�[A\A]]�fDH�,DH�5��1�����H��1�[A\A]]�fD��H��(H��tH�	�(��UH��SH���(H��H���8�����uH���(H�]����蛑��H��H�����H���(H�]�����UH��AUI��ATI��SH��H��dH�%(H�E�1�H�E��Z���H����H��H�H��tH;0tH��������a����H��M���I�$H��tH90tL���z������M���L��H�u��-��I���5���H�}к�������t1H�}�H��t�3���H�E�dH+%(�H��[A\A]]�DL�-!�(H�{8L������I�$I�$H�{(H�C(H�C8H��t����I�D$�5��(H��1�I�D$H�C(1��c���I�|$I�D$H��t���I�<$L�����L���7����A���L�BH�
���1�H�5jY�q��L�\H�
���1�H�5IY�P��L��[H�
Ҝ��1�H�5(Y�/���j��f.���UH��ATI��SH���:��M����H��I�$H��tH90tL��������t�_���H��H����H�H��tH;0tH�������tv�� ��H���(H�5b�(H�=��(H��H�����H�C����H�{H��[���H��L��[H�m���A\]�u���L�[H�
����1�H�5=X�D��L�E�H�
֛��1�H�5X�#����UH��SH��H�����H��tMH��H�H��tH;0tH��������t1�{@��u#H��H�5��������C@H�]���fD�����L�qZH�
���1�H�5�W����UH��AVAUATI��SH�����H����H��H�H��tH;0tH���I������M����1�H�5lWL��1��5���H��I�����1�1�1�H��I���9��I��H��t8H�Ǿ��4��H�5�?L��H��A�H�������H�{ L������M��tL��茭��[L��A\A]A^]�\���@[1�A\A]A^]�I���L�aYH�
ۙ��1�H�5�V������UH��AUATI��SH��H�=m�(谾��I�D$ ����L��H������6���H�0H��t&I��1�f�L������CI�t�H��H��u�H��(H�5��(H�=��(H���K���I�D$(H���έ���)��L��A�H�����I�D$H��H�5�>������I�D$8H��[A\A]]���UH��SH��H������H��t&H��H�H��tH;0tH���n����t
H�C8H�]���H��WH�5r�1��ˮ��1������UH��AUI��ATSH��H�����H��tgH��H�H��tH;0tH�������tKH�[8H��u�U�H�[H��tDL�#L���\��L��H���a���u�H��L��[A\A]]��H�BWH�5��1��#���H��E1�[L��A\A]]�f���UH��ATI��SH���
���H��t=H��H�H��tH;0tH���]����t!M��t1H�{(L���H��[A\H��]���@H��VH�5��1�裭��[1�A\]�ff.����H�m�(H��tH�a�(��UH��SH�L�(H��H���ش����uH�5�(H�]�����{���H��H���0��H��(H�]�����UH��SH��H���{���H��H�]�1��H��H�5�S1�逪����UH��AVAUATSH��H��dH�%(H�E�1��3���H���:H��H�H��tH;0tH���"�����H�{�a��I��H���F��L�����H��1�1�jH��A�L��jA�j���H�� L��BH�C8H��葸��L����������H�{8��1�H�5�UL���ź��H�{8H�C ��H�{����1�H�5�UI��H��虺��A���� L��L��0����I���f����H��8�������H��p���L��E1�L��H��p���H�s8jA� P� 臽��XZH�{8�zH���°��L���*��H�S8H�s 1�L������L���0������(Džp���!L������1�H�5�TL��H�E��ѹ��L���E� H�E�H�C H�E�H�E�H�C8H�E�H�E�H�E�����1�L��L��p���H�����1�H�5�TL���l���1�H�5�TL��H�C(�W���H�{1�H��H�C0H�5�����CP�� @H��SH�5�H�=<Q�f���1�H�U�dH+%(��H�e�[A\A]A^]�@H��SH�5b�H�=�P�&�������H�s8L���D��H�C8�f.�H��SH�5b�H�=�P����o����Ic��H��I��H�x@����������ff.���UH��AWM��AVI��AUI��ATI��SH��H�����H����H��H�H��tH;0tH����������H�{@L���b����u>A�$H�{8�C@A�E�CDA��CHA��CLt]H��H��[A\A]A^A_]�g����H�{DL�������t�H�{HL�������t�H�{LL��������t�H��[A\A]A^A_]ÐH�{RH�52�H��H�=�O[A\A]A^A_]騨���H�RH�5������H�5�(H��tH�)�(��UH��SH��(H��H���د����uH���(H�]�������H��H���0��H���(H�]���UH�= RH��SH��軿��H���s���H��H��L�
���jH��A� ��H�
����i���H�]�����H���(H��tH���(��UH��SH���(H��H��������uH���(H�]�����[���H��H���p���H���(H�]�����UH��AUATI��SH��H��dH�%(H�E�1�����H����H��H�H��tH;0tH���������M��ttH������I��H�����L��L��H��P�������L��A��贾��E��tY���1�H��H�5�MH��1��D���H�U�dH+%(uSH�Ę[A\A]]��H��PH�5��H�=LM�v���1��f�H�)uH�5��H�=,M�V���1������ff.�f���UH��SH��H�����H�<�(�2�(���~H�����H�
ا��H�C0H����fHn�fHn�fl�C����H�3E1�E1�P1ɺH�=�Oj1�jj葵��H�� ���(���H�3E1�E1�P1ɺH�=�Oj1�jj�]���H�� ���(���H��H�3E1�j E1�1ɺj H�=�Oj@P1�jjj����H��@�M�(�p���H��H�3E1�j E1�1ɺPH�=yO1�jjj���H��(H�3E1�jE1�1ɺjH�=_Oj���(1�跴��H�� ���(踣��H�=�KA�H��H��H���-��H��H�]�H���(H���(�����DH�5��(H���ɥ���n���@���颰��f���HcA�(H�D8�ff.�@��UH��ATI��SH������H�{`H������5,�(L��1�H��1��I���[1�A\]�f���UH��AWAVAUATI��SH��H��(���!t&��uH�B8H9F �H�e�[A\A]A^A_]�@H�v H;r8u�H�C(H;B(�I;D$0u�M�l$XM��u��DM�mM��t�M�uI;v(u�I�V�I�~H�s8H9�HN�H)�I~ Lc�L���[���M)~�v���I�v(I�|$`�ù��H��t&H��M�M�F A�v�51�(H��1�1�L���J���XZI�~ �?���L���7���I�|$XL���ʣ��L��I�D$XH�e�[A\A]A^A_]�O�����5��(1�1�L�����I�|$8����H�e�L��[A\A]A^A_]�D���@H�S@H����H���JH������M�l$XH�CHM��u�N�M�mM��t>M�uI;v(u�I;u�I�~ �w���L���o���I�|$XL������L��I�D$X蕳��H�s I�|$`藸��H���3���L�CH�5�(H�e�1�[H��L��1�A\A]A^A_]�����H�[HI�|$`H���O���H�����I�|$H���Y���I��H�������5��(H��1�1�L���Ȯ��E1�E1�L��H�8���H�5{HL���9���L��H�����L�����H���FI�|$`H�e�L��H��[A\A]A^A_]�?���I�|$`起��H��H���N���H�CHM�l$XL�{PL�KXH�E�M��tbH�C �f�M�mM��tJM�uI;F(u�M;u�I�~ L�M�H�M�����L�����I�|$XL��蒡��L��I�D$X�%���H�M�L�M�M��u'H���uȋ5��(1�L��KL��1�踭��Y^����0L�M��s��L�M�fIn�I�I��H�C fIn�fl�I�E(AEfIn�E�AE�ʯ��I�UI�|$XL��I�E �葺��I�D$X�<����5�(H�e�L��L��[1�A\A]A^A_]�&���fD��Hc��(H�D8�ff.�@��UH��SH��Hc��(H�9s t�CL�s �S$��tH�]��Ð9S$t�CL�s �S$��u�H��H�=��������CLH�]���fD��HcA�(H����UH��AVAUATSH��H��0dH�%(H�E�1��f���H���-H��H�H��tH;0tH��蕿�����
H�����H���ͽ��1�H�5lII��H��虭��1�H�5dIL��I��腭��L��I�����H���R���E1�1�L��I��������H��L��H�E�PH�E�PH�E�PH�E�PH�E�PAU�J���H��0L����\���	�uRH�}�L9m�u>�}�u8H�u�H��t/1��H����ttH�}�H�u��W���H�}�H��苼���f�H��t�v���1�H�E�dH+%(u;H�e�H��[A\A]A^]�DH��HH�5�H�=�D����@H�}��襷��D��UH��AWAVAUI��ATI��SH��H��(dH�%(H�E�1�����M���EH��I�EH��tH;0tL��������$L�����L��I����f�L��)E�I������I��H�����L��H�U�L���Ԧ��L������H��tH�L�u�M���I�$M��tC1�诡��A�6I��@��tL��I���$���A�6@��u�L���c���L�u�I�$L������L�e�M��tGH��t:1��^���A�4$I��@��t�L��I���ԥ��A�4$@��u�L������L�e�H�L���ú��H�E�dH+%(ubH��([A\A]A^A_]�DH�E�dH+%(u?H��(H��FH�5�[H�=�BA\A]A^A_]����f�M���<����?����͵��ff.�f���UH��AUATI��SH��8dH�%(H�E�1�H�E��P���H��H��腺��H���}���H��H���b��1�H�5CFH���A���L��I������E1�1�L��I��������H��H��H�E�PH�E�PH�E�PH�E�PH�E�Pj����H��0H��A������D	�t2H�}�1�H��t�z���H�E�dH+%(u)H�e��[A\A]]�fDH�}�u�H�}�u�H�}����轴��ff.�f���Hc��(H�H��t�G(�H��t�G,��ff.�f���UH��AWI��H��AVAUI��ATSH��XdH�%(H�E�1��{���A�ƃ���v
A����L�����H���&���L��I������H��H����H������H������L���I���L��L��`�������L��貼��L��Dž`���H�]�H�E�H������H�E��i���M��H��L�狕�������������E�H�E����Љ����fn�H��w1��fn�1�H�E�fb�fl�)E�譛��A��L�������\foU�H�E�L��L��(���H�� ���)�@���HDž0���H��8�������L��L�����DžX�����P����U���1�1�M����H��L��Dž�����T���� ���A���v1�1�L��M��Dž`���H�����L��貰��H�E�dH+%(�KH��X[A\A]A^A_]�DH�E�dH+%(�!H��XH�ogH�5@�[H�=I?A\A]A^A_]�j���f.�H�E�dH+%(��H��XH��g�1�[H�=?A\A]A^A_]�B���f�fo]�H�E�L��L�����H������)���HDž���H�����記��L��L������Dž�������Dž��������M��1�1�H��L�牅��Ǚ��Dž����M��1�1�H��L��諙�����fDDž������?���ff.�@��Hc1�(H�9p8u9P<t�p8�P<�]���D����UH��AUATSH��Hc��(H�H�{��L�SPM���|�S|E1��t2H�;躵��H�sL��H��I������H�sH��L��[A\A]]����f�A�JE1������f��f��f��tI�z �*��F�*��F�*��^��^��^�H����M�J(E1�f��M���1A���NL��1�f�H����t�1�@H���u��f������*�M�R0f��M��t@L��E1�A��uH��A���t�DH����u��Ѹf��D������*��Y�D��L	��Y�L	��Y�L	��,��D,�����,�A���D���D�$0Mc�I)�I������H��[A\A]]�D@��uzH��E1�@H��A���t�1�H���u��M�J(f������*�M������1�f������@A�����A���Z���f.�1�L�����fDE1�H������UH��AWAVAUATSH��(H����A�����}�Ӆ��sI��赥��1�A��D��ƺA���
������L��H�E�������TL���<���A�������
1���D���+���H��I��谘���H��I���@���f��L��L��f(�����L���E���L���=���L���U���L���
�������H�}���L�u�L���n�����L��辿��L��Lc�����L��L�u�H�E��#���H�}�Lc�觿��L�E�E1�H��D1��@1�f��1��D�@�t�H��A9�~vA�����t�A�����A�����)�1�D����A�T����)�1�D���D�A�����)�1�D���A������������f�A��M�L�D9��U���L������H�E�H��([A\A]A^A_]�f�L��E�赾��L��Lc��
���H�}�H�E�����H�}�Lc�衾��H�u�D�E�H��fDH��1��T�H���Q����q���H���Q�A9��A��L�L�D9���f������7���L���қ��I���-���H��<H�5z1��%���H�E��3���L���0�����L���&���H�}�H��t��H�����H��<H�5�y1�����ff.�@��UH��AVAUI��ATI��S�6���H��tQH��H�L��L�pL��������uH��[A\A]A^]�@M��M��L��H��`1�1�1�舘����fDL��H�4<1�1��1��f����@��UH��AVAUI��ATI��S覧��H��tQL�pL��H��L��������uH��[A\A]A^]��M��M��L��H��`1�1�1������fDL��H�~`1�1��1��֗���@��UH��AWAVI��H�=�;AUATSH��8H�u�dH�%(H�E�1��Y���H����I��L�m�f�L���ț��H��H��tpH�E��{.t��]���H�{�
L���I��賲��H��A���u�H�E�H��t��8u�Hc�H9�u�L��軙��9�t�H�}���A�օ�t�f.�H�E�dH+%(upH��8L��[A\A]A^A_]髷��H�u���-�����uMH�]�H���tC��~E1��	A��A9�tH�}�D��A�օ�t�H�E�dH+%(uH��8[A\A]A^A_]��,�����b������ff.���UH��AVAUATS���J���H���2W��I��H��tKI���M�,$L���Tm��H��H��tSH����H�RH��t79u�����M��u
M�d$M��u�E1�L�����[L��A\A]A^]�������1��������UH���s_��H����H�����]H�����f.���UH��SH��H���;_��H����H��H�]�A��H��H��H�589鿿��ff.�@��UH����^��]H���>��f.���UH��AWAVL�}�AUATI��SH��H��hdH�%(H�E�1��^��H�����H�����H��I��蒶��L��I���Ǥ��H�U�H�u�L��A��蔘��C�vf�L�����]��U�L�u��*�(��\�(��^��\��Y��\���~/kY��A~$X�U�芷���C0L��L��L��L�e��S���L��H���ر��L��L��L���ڵ��H��袥��L��H��I����H�u�H�U�L������S@f�CD�U�D�{Tf��L���*��]�D�sPA)�A)��^�f��*��CL+C<�*ȋCH+C8�^�f��*���x����Y��Y���|���肕��f��f�L���A*���|�����x����A*��Y��Y��XM��XE�膶��L���Τ��L��H���#���H�E�dH+%(uH��h[A\A]A^A_]��Х����UH��AWI��AVAUL�m�ATSH��H��H�?dH�%(H�E�1�H�E����H�U�H��|���H��I��艧��fn}�I�?H�E�fn�|���fb�f�E���H�M�H�U�L��H��L�E�I���)����E�L��L��H�M���AG<�Z���H���L��I���֘��H��I������H���'H��H��h������H��h�����`���H��H��P���������h�������H��������h�����`���H��H��X����	���H��H��h����*���f�۾(�(�(�H��H��`����i���H��X����M���H��P���1�H��H��X������f���=�g�UhH��X���H��`���(�(�(�(�(�诰��H��X���胇��H��`����w���H��h���1��Y���H��h���I�G@�Y���L��聋��I�wH1�L���AGP�����AGH�AOLL���I���H�U�H�u�L��I���v���fnE�fnu��AOPfb����[�Y��A~OH\�AOHM����H�u�L��赳��L���-���f����g�-Qg�*M�(��AGH�\�(��Y�(�T�.���f���AOL�*]��\��Y�(�T�.�w�(��~U����
�f^�[�X�AWHH���(L��H��艐��H��tH���,���H�}�H��t�N���H�E�dH+%(��H�Ĉ[A\A]A^A_]�f��,�f���5fU��*�(����T��\�V�(��P�����,�f���=�eU��*�(����T��\�V�(�����H�u�H��H�E�茉��H���/����2����١��f���UfAn�fn�fn�fn�fb�fb�H��AWfl�AVL�}�AUI��ATE��SA��H��XH�?dH�%(H�E�1�)E����H�M�L�E�L��foE�H�U�H��I��H�E�)E��:����U��u�1��M���H��H���R���H�߉E�觙��H���M�L��H��H�E��E�L��PE�L$A���}���ZY��t?I�] �ަ��H�}�I�EH��t茉��H�E�dH+%(uEH�e�[A\A]A^A_]�fDH������H�E�1��H��1H�H1����H�}�H��u��胠����UH��AUATSH��H���wW��I��H����L��I�����1�H�������s(H�S8H������s(H��H�SHL��[A\A]]鯗��ff.�@��UH��SH��H��H���(�PHH��H�]���i���f���UH����V��]H���J<��f.���UH��ATSH��H��tM�v��H��1�H�5
I��1����L��H��H���q��I��H��tH��褂��I��H���ɂ��L��[A\]ÐH��+H�5Rm1�E1��x�����fD��H�UUUUUUUUH��H��H!�H)�H�33333333H��H��H!�H!�H�H��H��H�H�H!�H�H��H��8���H��H���app->running_state->windows../src/shell-app.csrc/org-gtk-application.csrc/switcheroo-control.creturn_value != NULLn_param_values == 3n_param_values == 5property../src/shell-camera-monitor.cShellAppShellAppUsageShellCameraMonitorShellGlobalhandle-activatehandle-openhandle-command-lineBusyborg.gtk.Application(ssv)net.hadess.SwitcherooControl{&sv}ShellAppSystemHasDualGpuNo property with name %sswitcheroo-control vanishedas(sa{sv}as)PropertiesChangedhas-dual-gpuNumGPUsnum-gpusaa{sv}GnomeShellPluginCameras in usecameras-in-useapp->running_state != NULL.desktopActivateActionorg.freedesktop.Application %s="application-statecontextscorelast-seenUnknown element <%s>../src/shell-blur-effect.cShellBlurEffectself->actor != NULLShellBlurEffect (brightness)ShellBlurEffect (blur)ShellBlurEffect (downscale)ShellBlurEffect (background)ShellBlurEffect (blit)ShellBlurEffect (final)brightnessPipeWire:Interface:Nodemedia.roleCamera[gnome-shell] PipeWire/usr/share/gnome-shellGNOME_SHELL_DATADIRGNOME_SHELL_JSimages/%s/LEorg.gnome.shell:resourcesearch-pathShellActionModeShellAppStateShellAppLaunchGpuShellBlurModeRadius in pixelsRadiusradiusBrightnessBlur modeShellSnippetHookShellNetworkAgentResponseShellOrgGtkApplicationShellOrgGtkApplicationProxy(@a{sv})Activate()(^ass@a{sv})Open(o^aay@a{sv})CommandLine(i)g-interface-nameg-object-pathg-connectiong-bus-typeswitcheroo-control appeared/net/hadess/SwitcherooControl(ss)GetApplication stateBusy stateApplication idGIconApplication Action Groupaction-groupDesktopAppInfoapp-infoSHELL_IS_APP (app)notify::busyStatusChanged(u)application-x-executableprogramUnknowngiconfallback-app-iconactionswinapp->running_state->muxerapp.new-windowSingleMainWindowX-GNOME-SingleWindowG_IS_TASK (result)app-state-changedinstalled-changed../src/shell-app-system.cSHELL_IS_BLUR_EFFECT (self)the_object == NULLthe_objectSHELL_IS_GLOBAL (global)window-managerapp.quitavworkspace >= -1shell_app_activate_actiondesktop-startup-idactivation-token(s@av@a{sv})PrefersNonDefaultGPUa{s*}DefaultEnvironmentFailed to launch “%s”app->info != NULLgnome-workspace-switchedapp->running_state == NULLnotify::user-timenotify::skip-taskbarwindow:%dstate->refcount > 0(name unknown)(filename unknown)startingrunning  <context id="">
    <application%u/>
  </context>
</application-state>
notify::focus-apporg.gnome.SessionManagerg-signaluserdatadirapplication_stateorg.gnome.desktop.privacychanged::remember-app-usagenotify-errorlocate-pointershutdownuserThe session mode to useSession Modesession-modeScreen width, in pixelsScreen Widthscreen-widthScreen height, in pixelsScreen Heightscreen-heightMetaBackend objectBackendbackendMetaContext objectContextDisplayMetaCompositor objectCompositorcompositorWorkspace managerworkspace-managerStagestageActor holding window actorsTop Window Grouptop-window-groupWindow management interfaceWindow ManagerSettingssettingsData directoryImage directoryimagedirUser data directoryThe shell's StFocusManagerFocus managerfocus-managerFrame Timestampsframe-timestampsFrame Finish Timestampsframe-finish-timestampforce-animationsautomation-scriptThe debugging flagsDebug Flagsdebug-flags../src/shell-global.cexit_statusiplatform_dataargumentsaayhinturisfedora-mozilla-debian-GNOME Shell0.1VariousGPLv2+net-hadess-switcheroo-controlorg-gtk-applicationSHELL_NETWORK_AGENT_CONFIRMEDconfirmeduser-canceledinternal-errorSHELL_SNIPPET_HOOK_VERTEXvertexvertex-transformSHELL_SNIPPET_HOOK_FRAGMENTtexture-coord-transformlayer-fragmenttexture-lookupSHELL_BLUR_MODE_ACTORSHELL_BLUR_MODE_BACKGROUNDbackgroundSHELL_APP_LAUNCH_GPU_APP_PREFapp-prefSHELL_APP_LAUNCH_GPU_DISCRETEdiscreteSHELL_APP_LAUNCH_GPU_DEFAULTdefaultSHELL_APP_STATE_STOPPEDstoppedSHELL_APP_STATE_STARTINGSHELL_APP_STATE_RUNNINGSHELL_ACTION_MODE_NONEnoneSHELL_ACTION_MODE_NORMALnormalSHELL_ACTION_MODE_OVERVIEWoverviewSHELL_ACTION_MODE_LOCK_SCREENunlock-screenlogin-screensystem-modallooking-glassSHELL_ACTION_MODE_POPUPpopupSHELL_ACTION_MODE_ALLallscale-factorfd %d is not CLOEXECpid/snap/binsnap runG_IS_FILE (path)bytes != NULLreplace_contents_asyncG_IS_FILE (file)shell_util_touch_file_asyncchild != NULLpassword-visibleconfirm-visiblewarning-visiblechoice-visibleShellGLSLEffectShellInvertLightnessEffectShellPerfLogShellQrCodeGeneratorShellScreenshotShellTrayManagerShellWindowTrackershow-processes-2messagedescriptionwarningpassword-newpassword-strengthchoice-labelchoice-chosencaller-windowcontinue-labelcancel-labelPassword field is visiblePassword visibleConfirm field is visibleConfirm visibleWarning is visibleWarning visibleChoice is visibleChoice visibleText field for passwordPassword actorpassword-actorConfirm actorconfirm-actorshow-passwordshow-confirm../src/shell-window-preview.c../src/shell-window-tracker.cShellKeyringPromptG_VALUE_HOLDS_STRING (value)stripped_label != NULL../src/shell-keyring-prompt.cShellMountOperationEvent names can't include '"'perf.setTime\",
  [%li, "%s"][%li, "%s", %i][%li, "%s", %li][%li, "%s", "%s"]../src/shell-perf-log.cinitiatecancelplug-addedscreenshot-taken%cscreenshot != NULL../src/shell-screenshot.c%FT%T%ztEXt::SoftwarepngtEXt::Creation Timegnome-screenshotShellSecureTextBufferShellSquareBinShellStackShellWindowPreviewThe icon's window titleTitleThe icon's window WM_CLASSWM Class../src/shell-tray-icon.cShellTrayIcontray-icon-addedtray-icon-removedBG Colorbg-colorwindow-container../src/shell-tray-manager.c(u&o&s&s)done(o)data->job == NULL../src/shell-util.cNot systemd managed/org/freedesktop/systemd1JobRemovedorg.freedesktop.systemd1Bounding Boxbounding-boxShellWindowPreviewLayout/proc/self/cmdlinefailed to reexec: %slaunchedglobal->work_count > 0klass->base_pipeline != NULLself->mode != PROMPTING_NONEself->task != NULLPasswords do not match.GNOME_KEYRING_PARANOIDPassword cannot be blankperf.statisticsCollectedclutter.stagePaintStartglFinishclutter.stagePaintDone[  ],
    "statistic": true }Qr code size mismatchon_image_task_completeG_IS_OUTPUT_STREAM (stream)after-paintshell_screenshot_screenshotshell_screenshot_pick_colorcolor != NULLtext-changedself->task == NULLshell-stop-pickLC_TIMEG_IS_TASK (res)StartUnitStopUnit(s)GetUnitREADY=1META_IS_WINDOW (window)size-changedposition-changeddestroywindow_info != NULLglobal->plugin == NULLnotify::widthnotify::heightStart of stage page repaintPaint completion on GPUx11-display-closingui-scaling-factor-changedCapturing window failedtray_icon != NULLwindow-createdreconfiguredNA_IS_TRAY_CHILD (tray_child)x11-display-setupstyle-changedFocused applicationFocus Appstartup-sequence-changedtracked-windows-changed%s.notify::wm-classnotify::titlenotify::gtk-application-idunmanagednotify::focus-windowunminimizesize-changemapkill-switch-workspacekill-window-effectsshow-tile-previewhide-tile-previewshow-window-menufilter-keybindingconfirm-display-changecreate-close-dialogShellWMShellAppCacheNaTrayManagermonitor-indexstate-adjustment-valueShellWorkspaceBackground../src/qrcodegen.c0 <= e && e < 4falsenew-requestcancel-requests_con../src/shell-network-agent.cconnection-uuidNaXembedShellNetworkAgentCanceled by NetworkManagersetting-keya{sa{sv}}{s@a{sv}}a{ss}setting != NULLsetting-nameNo plugin for %sshell_app_cache_do_updatesetting_names_con != NULLconnection_uuid != NULLconnection_id != NULLsetting_key != NULLNetwork secret for %s/%s/%sattrssettingservice_nameVPN %s secret for %s/%s/vpnsecretsfolders != NULL../src/shell-app-cache.cpath != NULLNameDesktop Entrydesktop-directoriesnotification_area_XEMBED_INFO too shortplug-removedx11-display../src/tray/na-tray-manager.c../src/tray/na-xembed.c_XEMBED_XEMBED_INFOWM_NORMAL_HINTS_NET_SYSTEM_TRAY_COLORSqrcode != NULLbits >> 15 == 0segs != NULL || len == 00 <= ccbits && ccbits <= 16bitLen == dataUsedBitsbitLen <= dataCapacityBitsbitLen % 8 == 0i == dataLen * 8bits >> 18 == 0text != NULLdata != NULL || len == 0result.bitLength != -1digits != NULLbitLen != -1'0' <= c && c <= '9'result.bitLength == bitLentemp != NULLSHELL_IS_NETWORK_AGENT (self)service != NULLrequest != NULLSHELL_IS_APP_CACHE (cache)user_data == NULLG_IS_TASK (task)SHELL_IS_APP_CACHE (self)NA_IS_TRAY_MANAGER (manager)_NET_SYSTEM_TRAY_S0_NET_SYSTEM_TRAY_VISUALmanager->window != NoneMANAGER_NET_SYSTEM_TRAY_OPCODE_NET_SYSTEM_TRAY_MESSAGE_DATANaTrayChildicon_window != Nonetray_icon_addedtray_icon_removedmessage_sentmessage_cancelledlost_selectionUTF8_STRING_NET_WM_NAMENA_IS_TRAY_CHILD (child)_NET_WM_PIDsurface != NULLwidth > 0 && height > 0Unknown statistic '%s'
/proc/self/fdworkareas-changedFailed to take screenshot: %sG_VALUE_TYPE (a) == G_VALUE_TYPE (b)_g_value_equal() does not handle type %s%s:%d: invalid %s id %u for "%s" of type '%s' in '%s'prop_id != 0 && prop_id - 1 < 1org.freedesktop.DBus.Properties.Setprop_id != 0 && prop_id - 1 < 3Error setting property '%s' on interface org.gtk.Application: %s (%s, %d)Error setting property '%s' on interface net.hadess.SwitcherooControl: %s (%s, %d)[generated] _shell_org_gtk_application_emit_changed[generated] _shell_net_hadess_switcheroo_control_emit_changedorg.freedesktop.DBus.PropertiesWhether any camera is currently used by an appMissing attribute id on <%s> elementCould not load applications usage data: %s../src/shell-blur-effect.c:208%s: Unable to create an Offscreen bufferShellBlurEffect (actor offscreen)ShellBlurEffect (actor texture)  cogl_color_out.rgb *= brightness;                                       
uniform float brightness;                                                 
pipewire_loop_iterate failed: %sFailed to start camera monitor%s/gnome-shell/runtime-state-%s.%sCould not get GPUs property from switcheroo-control: %sMethod %s is not implemented on interface %sShellOrgGtkApplicationSkeletonSHELL_IS_ORG_GTK_APPLICATION (object)ShellNetHadessSwitcherooControlShellNetHadessSwitcherooControlProxyShellNetHadessSwitcherooControlSkeletonSHELL_IS_NET_HADESS_SWITCHEROO_CONTROL (object)Could not get switcheroo-control GDBusProxy: %sGot switcheroo-control proxy successfullyThe desktop file id of this ShellAppThe GIcon representing this appThe action group exported by the remote applicationThe DesktopAppInfo associated with this app[gnome-shell] idle_save_application_usageg_async_result_is_tagged (result, shell_app_activate_action)G_IS_DESKTOP_APP_INFO (app->info)g_application_id_is_valid (g_app_info_get_id (G_APP_INFO (app->info)))action_name != NULL && action_name[0] != '\0'parameter == NULL || g_variant_is_of_type (parameter, G_VARIANT_TYPE ("av"))cancellable == NULL || G_IS_CANCELLABLE (cancellable)Could not apply discrete GPU environment, switcheroo-control not availableCould not apply discrete GPU environment, no GPUs in listCould not find discrete GPU in switcheroo-control, not applying environmentapp->running_state->session != NULLApp "%s" (%s) claims to still be %s when being disposed. Please ask the app developer to check if StartupNotify=true in the .desktop file is properly implemented.!(app->state == SHELL_APP_STATE_RUNNING && state == SHELL_APP_STATE_STARTING)Could not save applications usage data: %s<?xml version="1.0"?>
<application-state>
/org/gnome/SessionManager/PresenceMetacity display object for the shellStage holding the desktop scene graphActor holding override-redirect windowsGSettings instance for gnome-shell configurationDirectory containing gnome-shell data filesDirectory containing gnome-shell image filesDirectory containing gnome-shell user dataWhether to log frame timestamps in the performance logWhether at the end of a frame to call glFinish and log paintCompletedTimestampD-Bus Proxy for switcheroo-control daemonForce animations to be enabledAutomation script to run after startupProvides GNOME Shell core functionalitySHELL_NETWORK_AGENT_USER_CANCELEDSHELL_NETWORK_AGENT_INTERNAL_ERRORSHELL_SNIPPET_HOOK_VERTEX_TRANSFORMSHELL_SNIPPET_HOOK_TEXTURE_COORD_TRANSFORMSHELL_SNIPPET_HOOK_LAYER_FRAGMENTSHELL_SNIPPET_HOOK_TEXTURE_LOOKUPSHELL_ACTION_MODE_UNLOCK_SCREENSHELL_ACTION_MODE_LOGIN_SCREENSHELL_ACTION_MODE_SYSTEM_MODALSHELL_ACTION_MODE_LOOKING_GLASSCould not delete runtime/persistent state file: %s
!cancellable || G_IS_CANCELLABLE (cancellable)Could not replace runtime/persistent state file: %s
Failed to open runtime state: %sthis prompt can only show one prompt at a timeshell_keyring_prompt_password_asyncthis prompt is already promptingshell_keyring_prompt_confirm_asyncRGB = ADD (SRC_COLOR * (SRC_COLOR[A]), DST_COLOR * (1-SRC_COLOR[A]))cogl_texel = texture2D (cogl_sampler, cogl_tex_coord.st);
vec3 effect = vec3 (cogl_texel);

float maxColor = max (cogl_texel.r, max (cogl_texel.g, cogl_texel.b));
float minColor = min (cogl_texel.r, min (cogl_texel.g, cogl_texel.b));
float lightness = (maxColor + minColor) / 2.0;

float delta = (1.0 - lightness) - lightness;
effect.rgb = (effect.rgb + delta);

cogl_texel = vec4 (effect, cogl_texel.a);
Text field for confirming passwordg_task_get_source_object (G_TASK (result)) == promptg_async_result_is_tagged (result, shell_keyring_prompt_password_async)g_task_get_source_object (task) == promptg_async_result_is_tagged (result, shell_keyring_prompt_confirm_async)Only supported event signatures are '', 's', 'i', and 'x'
Maximum number of events defined
Duplicate event event for '%s'
Discarding oversize event '%s'
ShellPolkitAuthenticationAgentInvalid UTF-8 in username for uid %d. SkippingError looking up user name for uid %dUnsupporting identity of GType %s[gnome-shell] handle_cancelled_in_idleAuthentication dialog was dismissed by the usershell_screenshot_composite_to_streamThe PID of the icon's applicationBackground color (only if we don't have transparency)../src/shell-window-preview-layout.cSystemd job completed with status "%s"Could not issue '%s' systemd callError fetching own systemd unit: %sorg.freedesktop.systemd1.Managerfailed to get /proc/self/cmdline: %s[gnome-shell] run_leisure_functionsSHELL_IS_KEYRING_PROMPT (self)[gnome-shell] statistics_timeoutFinished collecting statisticsperf_log->events->len == EVENT_SET_TIME + 1perf_log->events->len == EVENT_STATISTICS_COLLECTED + 1failed to resolve required GL symbol "%s"
clutter.paintCompletedTimestampOnly supported statistic signatures are 'i' and 'x'
Unsupported signature in event{ "name": "%s",
    "description": "%s"PolKit failed to properly get our sessionSHELL_IS_POLKIT_AUTHENTICATION_AGENT (agent)agent->current_request != NULLSHELL_IS_QR_CODE_GENERATOR (self)No valid QR code uri is providedOnly one QR code generator operation at a time is permittedshell_qr_code_generator_generate_qr_codeg_async_result_is_tagged (result, shell_qr_code_generator_generate_qr_code)SHELL_IS_SCREENSHOT (screenshot)Only one screenshot operation at a time is permittedshell_screenshot_screenshot_areashell_screenshot_screenshot_stage_to_contentg_async_result_is_tagged (result, shell_screenshot_screenshot)g_async_result_is_tagged (result, shell_screenshot_screenshot_stage_to_content)g_async_result_is_tagged (result, shell_screenshot_screenshot_area)g_async_result_is_tagged (result, shell_screenshot_screenshot_window)g_async_result_is_tagged (result, shell_screenshot_pick_color)cairo_image_surface_get_format (priv->image) == CAIRO_FORMAT_ARGB32g_async_result_is_tagged (result, shell_screenshot_composite_to_stream)password_actor == NULL || CLUTTER_IS_TEXT (password_actor)confirm_actor == NULL || CLUTTER_IS_TEXT (confirm_actor)Unknown value of _NL_TIME_WEEK_1STDAY.
File %s contains invalid UTF-8Open fd CLOEXEC check completeSHELL_IS_WINDOW_PREVIEW_LAYOUT (self)End of frame, possibly including swap timeQRCode generation failed for url %sshell_screenshot_screenshot_windowg_hash_table_size (tracker->window_to_app) == 0create-inhibit-shortcuts-dialog../src/shell-workspace-background.c0 <= numBits && numBits <= 16 && (unsigned long)val >> numBits == 0qrcodegen_VERSION_MIN <= ver && ver <= qrcodegen_VERSION_MAX21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsizeqrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAXThe secret agent is going awayInternal error while retrieving secrets from the keyring (%s)The request could not be completed.  Keyring result: %s_XEMBED_INFO property has wrong type(qrcodegen_VERSION_MIN * 4 + 17) <= result && result <= (qrcodegen_VERSION_MAX * 4 + 17)0 <= (int)mask && (int)mask <= 7qrcodegen_VERSION_MIN <= minVersion && minVersion <= maxVersion && maxVersion <= qrcodegen_VERSION_MAX0 <= (int)ecl && (int)ecl <= 3 && -1 <= (int)mask && (int)mask <= 70 <= numChars && numChars <= INT16_MAX0 <= bitLength && bitLength <= INT16_MAX0 <= result && result <= INT16_MAX0 <= (int)ecl && (int)ecl < 4 && qrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAX1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:shell_network_agent_search_vpn_pluginNetwork dialog was canceled by the userAn internal error occurred while processing the request.SHELL_IS_APP_CACHE (source_object)META_IS_X11_DISPLAY (x11_display)event_type == CLUTTER_BUTTON_RELEASE || event_type == CLUTTER_KEY_PRESS || event_type == CLUTTER_KEY_RELEASEshell tray: plug window is goneStatistic '%s'; defined with signature '%s', used with '%s'
Discarding unknown event '%s'
Event '%s'; defined with signature '%s', used with '%s'
org.freedesktop.NetworkManager.Connection(g���f���f���f��Pg���f���f���f��pg���f���f���f���g���f���f���f���f���f���f���f���f���f���f���f���g���f���f���f���g���f���f���f���f���f���f���f���f���f���f���f���f���f���f���f���g���f���f���f��h���f���f���f���f���f���f���f���f���f���f���f���f���f���f���f���f���f���f���f��Hh��Th��h��h��h��|h��h��h��h���h��h��h��h���h��h��h��h��h��h��h��h��h��h��h��h���h��h��h��h���h��h��h��h��h��h��h��h��h��h��h��h��h��h��h��h��i��h��h��h��Di��h��h��h��h��h��h��h��h��h��h��h��h��h��h��h��h��h��h��h��ti��U�����8�H��6��t6��L6��$6���5���5���5���5��L5��,5��5���4���4���4��|4��L4��4���3���3���3��\3��,3��3���2���6��D7���6���6���6���6���6���6���6���6���6���6���6���6���6���6���6���6���7���7���6���7��8��\8��shell_global_set_stage_input_regionshell_global_get_shell_global_initshell_blur_effect_set_modeshell_blur_effect_get_modeshell_blur_effect_set_brightnessshell_blur_effect_get_brightnessshell_blur_effect_set_radiusshell_blur_effect_get_radiusshell_blur_effect_paint_node_shell_app_system_notify_app_state_changedshell_app_disposeshell_app_activate_action_finishshell_app_activate_actionunref_running_state_shell_app_remove_windowcreate_running_stateget_application_proxybusy_changed_cbshell_app_on_ws_switchshell_app_on_skip_taskbar_changedshell_app_sync_running_stateshell_app_state_transitionshell_app_on_user_time_changedshell_app_open_new_windowshell_app_activate_fullshell_app_update_window_actionswindow_backed_app_get_windowshell_app_get_iconshell_net_hadess_switcheroo_control_skeleton_set_propertyshell_net_hadess_switcheroo_control_skeleton_get_property_shell_net_hadess_switcheroo_control_skeleton_handle_set_property_shell_net_hadess_switcheroo_control_skeleton_handle_get_property_shell_net_hadess_switcheroo_control_skeleton_handle_method_callshell_net_hadess_switcheroo_control_proxy_set_propertyshell_net_hadess_switcheroo_control_proxy_get_propertyshell_net_hadess_switcheroo_control_get_gpusshell_net_hadess_switcheroo_control_get_num_gpusshell_net_hadess_switcheroo_control_get_has_dual_gpu_g_value_equalshell_org_gtk_application_skeleton_set_propertyshell_org_gtk_application_skeleton_get_property_shell_org_gtk_application_skeleton_handle_set_property_shell_org_gtk_application_skeleton_handle_get_property_shell_org_gtk_application_skeleton_handle_method_callshell_org_gtk_application_proxy_set_propertyshell_org_gtk_application_proxy_get_propertyshell_org_gtk_application_get_busy_g_dbus_codegen_marshal_BOOLEAN__OBJECT_STRING_BOXED_VARIANT_g_dbus_codegen_marshal_BOOLEAN__OBJECT_BOXED_STRING_VARIANT_g_dbus_codegen_marshal_BOOLEAN__OBJECT_VARIANT_g_value_equal�@�o@���?�?��@�?�?�?$@��������������??K�@��@A�C�?���resource:///org/org/gnome/shell,
    "statistic��������`������8���P���h���x�������`���������������������� ���0���H������t���������������������,���t�����������������$���T���on_shutdownshell_window_preview_layout_get_windowsshell_window_preview_layout_remove_windowshell_window_preview_layout_add_windowon_actor_destroyedon_systemd_call_cbshell_util_touch_file_finishshell_util_touch_file_asyncna_tray_icon_removedshell_tray_icon_set_childshell_tray_icon_newshell_screenshot_composite_to_stream_finishshell_screenshot_pick_color_finishshell_screenshot_pick_colorshell_screenshot_screenshot_window_finishshell_screenshot_screenshot_windowshell_screenshot_screenshot_area_finishshell_screenshot_screenshot_areashell_screenshot_screenshot_stage_to_content_finishshell_screenshot_screenshot_stage_to_contentshell_screenshot_screenshot_finishshell_screenshot_screenshotwrite_screenshot_threadshell_qr_code_generator_generate_qr_code_finishshell_qr_code_generator_generate_qr_code�����shell_polkit_authentication_agent_completereplay_to_jsonshell_perf_log_initshell_keyring_prompt_cancelshell_keyring_prompt_completeshell_keyring_prompt_set_confirm_actorshell_keyring_prompt_set_password_actorshell_keyring_prompt_get_confirm_actorshell_keyring_prompt_get_password_actorshell_keyring_prompt_confirm_finishshell_keyring_prompt_password_finishshell_keyring_prompt_disposeremove_mnemonicsshell_glsl_effect_add_glsl_snippetshell_global_set_debug_flagsshell_global_get_debug_flagsreplace_contents_asyncshell_global_get_session_modeshell_global_end_work_shell_global_set_pluginshell_global_get_window_actorst���0���@���t���P���t���t���p���`���l*���+��,���)���*���*��l+��T,���)���)���)���)���)��H4��89��7��h6���5��x8���7��5��get_app_from_idutil_pixbuf_from_surfacena_tray_manager_set_colorsna_tray_manager_set_visual_propertyna_tray_manager_set_colors_propertyna_tray_manager_managena_tray_child_emulate_eventna_tray_child_get_wm_classna_tray_child_get_titlena_tray_child_newshell_app_cache_translate_foldershell_app_cache_get_infoshell_app_cache_get_allmonitor_desktop_directories_for_data_dirshell_app_cache_queue_updateapply_update_cbshell_app_cache_workerload_foldersload_foldershell_network_agent_delete_secretsvpn_secret_iter_cbcreate_keyring_add_attr_listsave_one_secretshell_network_agent_search_vpn_plugin_finishshell_network_agent_search_vpn_pluginshell_network_agent_respondshell_network_agent_set_passwordshell_network_agent_add_vpn_secretis_connection_always_ask
	

numCharCountBitsqrcodegen_makeEciqrcodegen_makeAlphanumericqrcodegen_makeNumericqrcodegen_makeBytesqrcodegen_isNumericqrcodegen_isAlphanumericsetModuleqrcodegen_getModuleqrcodegen_getSizegetModuleapplyMaskdrawFormatBitsgetNumDataCodewordsgetNumRawDataModulesgetTotalBitscalcReedSolomonGenerator�
�
�
��		

�		


!#%&(+-/1�
""#&(+-0358;>AD�" #%(*-0369<?BFJMQaddEccAndInterleavedrawCodewordsdrawWhiteFunctionModulesqrcodegen_encodeSegmentsAdvancedappendBitsToBuffercalcSegmentBitLengthGVariantd(�

 !!$%(*,.13445569;;;;=====?@AADEEFFGJJKLPQRSTWWWWWXYZ^_``ccceehijjkkkklnoqqrrrssuwwy{||~~��������������������������������������ߒn�dvp�i^��t��iL�i$j�*�ܕ$jv0j^�-,��^�	vh�z���fz�v��p�*#��mp�
v����x�8m��
v���s�ۗ��	v��=�s�?m=�vP����],����v��	!�o��	!v!<B�h��<BvHB��L�"��v����}ٕ�v�Y�V���Y�Ld�l�r_�
�l�Lt��7��I�v�����*?���v��TA�jXomTA
vhAaI�e�CmaIvxI�`��/Օ�`v�`3�f�3v03�?5��f�?
v�?Ib[��G�IbvhbngCn
vn�r`[����rv�r'�>��X�'�v8�T����r�T�v`�3�7�3�v@����5���v����=i��v�E�4�E
vE_tdwm_
v_hKP�dhL$h(h�X�>(hv8h�n���
v �D�g���D�vP�A�g>LnA�vX����5�m��	v��N�V��mN�v`�a���VavpO$���Ov`���v��,����v�.8^��.8vH8u~# XՕu~v�~ߛTS�.�ߛv��,	�Nř�,	v�,	4	8��̕4	v4	�K	��!��K		v�K	�O	��!���O	
v�O	��	��	���	v�	��	O��z���	v��	4Q
��Á�4Q
vHQ
>}
?k��>}
	vH}
��
*�pE���
	v��
j$`�+	mj$
vx$0�k$�0v00^zm��^v^H�D#d�H�vX���G�C��v�~����~�v��l��,[L�l�Lx���Y�ϕ��
v��N�����N�	vX��W�砕�v0���������v����X��v����uk���
v���Z[F���Zv�ZBfӁt�BfvXf/u��Xdm/uv@uِ��N�ِv�DIx��Dv0D_�3�B6m_�vp��c
�7���c
v�cS	�W�.�S	vh	�v)�k���vv�v��U�<����v��w��Ҹ!�w�v���F|��3��F
v�F�m��V���mv�m{08��{v({z�/�:�z�v��v��_�v
v�[�ml[v([�h��
��hv�h�}+��}v�}֞IR�m֞v�1��&X1�v@���nZ�����
v�"���͕"�
v0�F�����F�vX��@�\���@v�@�����m��
v����Kz����v��D	"J̕D	v`	
��X��
�
v���q[0ԕ��v��od���x�odv�d�Ե�����L����n�v���f-��v�>'5s��>	v�>�������v��H&=�z�H&	vX&NC�-�NCv`C�q/}��q
v�q~�J��~v~��x�(��L$�p���i{>p�v������FA���v����ú]���v����5�,��v���q�Z��v�^�_˾^vh@/ڸ}m@/	vP/jk>(�:jkvxk�x�t0�xvyי�!T�יv���E�
v GhQ��Gv(G�Kj�ے��Kv�K�V��S�Vv�V]���>��]�vp���%ĕ�v��4����4v�4)6���)6v@6�>�sp8��>
v�>�c-�9m�cv�cY��n�Y�vh�>�E���>�vH�>( Ju�>( vP( PP k��ymPP v`P �k ���3�k v�k �� �XH(��� v�� �� �Fq�m�� v�� �� ~a���� v�� ��!�����!	v�!0�!딏3>0�!vH�!��!\ow����!v��!�
";�
���
"v�
"�7"
��;>�7"v�7"�U"5��U"vV"�Z"�����Z"v�Z"�i"�"m�i"v�i"��"P[���"
v��")#��$0!)#L0#4#�T"4#
vH#;#D԰��;#L;#t<#G֐mt<#	v�<#ej#Xᩦ�ej#vxj#�$]��-��$v$j$���Uj$v�$�$�P�(�$v��$|�%�fN�|�%v��%�:&iI�1�:&v�:&3O&+���m3O&	v@O&�Q&Ȟ�l�Q&
v�Q&X&�ܯ?�X&v0X&s&����s&	v(s&,�&��k�>,�&
v8�&H	'7,p��H	'LP	'`	'�O��`	'vx	'?'B��N�?'vP't='�9�•t='
v�='!X']-��!X'v8X'w�'authd.js^W// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GLib from 'gi://GLib';
import {MessageType} from './util.js';
import {authd as Authd, gdm as AuthdGdm, pam as AuthdPam} from './authdProtocol.min.js';
import {UnifiedAuthService} from './unifiedMechanism.js';
import * as Const from './const.js';

export const SERVICE_NAME = 'gdm-authd';

const STRING_PROTOCOL_NAME = 'com.ubuntu.authd.gdm';
const STRING_PROTOCOL_VERSION_ = 1;

const AuthMechanismIDs = Object.freeze({
    BrokerSelection: 'authd-broker-selection',
    AuthModeSelection: 'authd-auth-mode-selection',
});


// FIXME: Add to proto!
const AuthResult = Object.freeze({
    Granted: 'granted',
    Denied: 'denied',
    Cancelled: 'cancelled',
    Retry: 'retry',
    Next: 'next',
});

// MOVE To proto?!
const UILayoutTypes = Object.freeze({
    Form: 'form',
    NewPassword: 'newpassword',
    QrCode: 'qrcode',
});

const EntryTypes = Object.freeze({
    Chars: 'chars',
    CharsPassword: 'chars_password',
    Digits: 'digits',
    DigitsPassword: 'digits_password',
});

function isEntryPassword(entryType) {
    switch (entryType) {
    case EntryTypes.Chars:
    case EntryTypes.Digits:
        return false;
    default:
        return true;
    }
}

function isEntryPIN(entryType) {
    switch (entryType) {
    case EntryTypes.Digits:
    case EntryTypes.DigitsPassword:
        return true;
    default:
        return false;
    }
}

export class AuthdSwitchableHandler extends UnifiedAuthService {
    get protocolName() {
        return STRING_PROTOCOL_NAME;
    }

    getSupportedRoles() {
        return [
            Const.PASSWORD_ROLE_NAME,
            Const.WEB_LOGIN_ROLE_NAME,
            Const.CHOICE_LIST_ROLE_NAME,
            Const.PLAIN_TEXT_ROLE_NAME,
            Const.MESSAGE_ROLE_NAME,
        ];
    }

    constructor() {
        super();
        this._brokers = {};
        this._authModes = {};
        this._authMechanisms = {};
        this._pendingEvents = [];
        this._stage = undefined;
        this._pendingNewChallenge = null;
    }

    handleProtocolRequest(protocol, version, json) {
        const authdData = AuthdGdm.Data.fromObject(JSON.parse(json));
        const reply = this._handleAuthdProtocol(authdData);

        this.sendProtocolResponse(reply.toJSON());
    }

    getProtocolResponse(_mechanism, _role, _response) {
        throw new Error('This should not be called!');
    }

    _handleAuthdProtocol(authdData) {
        switch (authdData.type) {
        case AuthdGdm.DataType.hello:
            console.log('Starting authd protocol');
            return new AuthdGdm.Data({
                type: AuthdGdm.DataType.hello,
                hello: new AuthdGdm.HelloData({version: 1}),
            });
        case AuthdGdm.DataType.poll: {
            /* gather the events happened meanwhile awaiting... here... */
            const pendingEvents = this._pendingEvents;
            this._pendingEvents = [];
            return new AuthdGdm.Data({
                type: AuthdGdm.DataType.pollResponse,
                pollResponse: pendingEvents,
            });
        }
        case AuthdGdm.DataType.request:
            return new AuthdGdm.Data({
                type: AuthdGdm.DataType.response,
                response: new AuthdGdm.ResponseData({
                    type: authdData.request.type,
                    ...this._handleAuthdRequest(authdData.request),
                }),
            });
        case AuthdGdm.DataType.event:
            this._handleAuthdEvent(authdData.event);
            return new AuthdGdm.Data({
                type: AuthdGdm.DataType.eventAck,
            });
        default:
            throw new Error(`Unhandled type ${authdData.type}`);
        }
    }

    _handleAuthdRequest(request) {
        switch (request.type) {
        case AuthdGdm.RequestType.uiLayoutCapabilities: {
            const supportedEntries = Object.values(EntryTypes).join(',');
            const supportedWait = [true, false].join(',');
            return {
                uiLayoutCapabilities: new AuthdGdm.Responses.UiLayoutCapabilities({
                    supportedUiLayouts: [
                        new Authd.UILayout({
                            type: UILayoutTypes.Form,
                            label: 'required',
                            wait: `optional:${supportedWait}`,
                            entry: `optional:${supportedEntries}`,
                        }),
                        new Authd.UILayout({
                            type: UILayoutTypes.NewPassword,
                            label: 'required',
                            entry: `optional:${supportedEntries}`,
                        }),
                        new Authd.UILayout({
                            type: UILayoutTypes.QrCode,
                            content: 'required',
                            wait: `required:${supportedWait}`,
                            label: 'required',
                            button: 'optional',
                        }),
                    ],
                }),
            };
        }

        case AuthdGdm.RequestType.changeStage: {
            // FIXME: Check is valid value of AuthdPam.Stage
            this._onStageChanged(request.changeStage.stage);
            return {ack: new AuthdGdm.Responses.Ack()};
        }

        default:
            throw new Error(`Unhandled request type ${request.type}`);
        }
    }

    setForegroundMechanism(mechanism) {
        if (this._selectedAuthMode === mechanism.id)
            return true;
        if (!this._authMechanisms[mechanism.id])
            return true;

        if (this._authenticationStarted) {
            this._pendingEvents.push(
                new AuthdGdm.EventData({
                    type: AuthdGdm.EventType.isAuthenticatedCancelled,
                    isAuthenticatedCancelled: new AuthdGdm.Events.IsAuthenticatedCancelled(),
                }),
                new AuthdGdm.EventData({
                    type: AuthdGdm.EventType.authModeSelected,
                    authModeSelected: new AuthdGdm.Events.AuthModeSelected({
                        authModeId: mechanism.id,
                    }),
                })
            );
            this._authenticationStarted = false;
        }

        return true;
    }

    handleMechanism(mechanism) {
        if (!this._selectedAuthMode || !this._authenticationInProgress)
            return mechanism.role !== Const.CHOICE_LIST_ROLE_NAME;

        if (this._selectedAuthMode !== mechanism.id)
            return true;

        return false;
    }

    _newPasswordConfirmed(newPassword) {
        if (this._pendingNewChallenge === newPassword)
            return true;

        if (!this._pendingNewChallenge?.length) {
            let msg = _('Please, type the new passphrase again');
            if (isEntryPIN(this._uiLayout.entry))
                msg = _('Please, type the new PIN again');
            this.emit('queue-message', msg, MessageType.INFO);
            this._pendingNewChallenge = newPassword;
        } else {
            let msg = _('The provided passphrases do not match, please try again');
            if (isEntryPIN(this._uiLayout.entry))
                msg = _('The provided PIN numbers do not match, please try again');
            this.emit('queue-message', msg, MessageType.ERROR);
            this._pendingNewChallenge = null;
        }

        return false;
    }

    handleAuthSelectionResponse(mechanism, role, response) {
        switch (role) {
        case Const.CHOICE_LIST_ROLE_NAME:
            this._onChoiceSelected(mechanism, response);
            break;

        case Const.PASSWORD_ROLE_NAME:
            if (this._uiLayout.type === UILayoutTypes.NewPassword &&
                !this._newPasswordConfirmed(response.password)) {
                this._showChallenge();
                break;
            }

            this._pendingEvents.push(
                new AuthdGdm.EventData({
                    type: AuthdGdm.EventType.isAuthenticatedRequested,
                    isAuthenticatedRequested: new AuthdGdm.Events.IsAuthenticatedRequested({
                        authenticationData: new Authd.IARequest.AuthenticationData({
                            challenge: response.password,
                        }),
                    }),
                }));
            break;

        case Const.PLAIN_TEXT_ROLE_NAME:
            if (this._uiLayout.type === UILayoutTypes.NewPassword &&
                !this._newPasswordConfirmed(response.text)) {
                this._showChallenge();
                break;
            }

            this._pendingEvents.push(
                new AuthdGdm.EventData({
                    type: AuthdGdm.EventType.isAuthenticatedRequested,
                    isAuthenticatedRequested: new AuthdGdm.Events.IsAuthenticatedRequested({
                        authenticationData: new Authd.IARequest.AuthenticationData({
                            challenge: response.text,
                        }),
                    }),
                }));
            break;

        case Const.WEB_LOGIN_ROLE_NAME:
            this._pendingEvents.push(
                new AuthdGdm.EventData({
                    type: AuthdGdm.EventType.isAuthenticatedRequested,
                    isAuthenticatedRequested: new AuthdGdm.Events.IsAuthenticatedRequested({
                        authenticationData: new Authd.IARequest.AuthenticationData({
                            wait: 'true',
                        }),
                    }),
                }));
            break;
        }


        return true;
    }

    _doStageChange(stage) {
        this._onStageChanged(stage);
        this._requestStageChange(stage);
    }

    _requestStageChange(stage) {
        this._pendingEvents.push(new AuthdGdm.EventData({
            type: AuthdGdm.EventType.stageChanged,
            stageChanged: new AuthdGdm.Events.StageChanged({stage}),
        }));
    }

    _maybeStartBrokerSelection() {
        if (this._stage !== AuthdPam.Stage.brokerSelection)
            return;
        if (!Object.keys(this._brokers).length)
            return;

        this._showChoiceList(AuthMechanismIDs.BrokerSelection,
            _('Select the broker'), this._brokers);
    }

    _maybeStartAuthModeSelection() {
        if (this._stage !== AuthdPam.Stage.authModeSelection)
            return;

        if (!Object.keys(this._authModes).length)
            return;

        if (this._authenticationInProgress || this._authenticationStarted)
            return;

        this._showChoiceList(AuthMechanismIDs.AuthModeSelection,
            _('Select the authentication mode'), this._authModes);
    }

    _showChoiceList(id, prompt, choices) {
        const choiceListMechanism = {
            name: 'Choices list',
            selectable: false,
            role: Const.CHOICE_LIST_ROLE_NAME,
            serviceName: SERVICE_NAME,
            protocol: this.protocolName,
            id,
            prompt,
            choices,
        };

        this.emit('mechanisms-changed', {[id]: choiceListMechanism});
        this.emit('start-mechanism', choiceListMechanism);
    }

    _onChoiceSelected(mechanism, key) {
        switch (mechanism.id) {
        case AuthMechanismIDs.BrokerSelection:
            this._pendingEvents.push(new AuthdGdm.EventData({
                type: AuthdGdm.EventType.brokerSelected,
                brokerSelected: new AuthdGdm.Events.BrokerSelected({
                    brokerId: key,
                }),
            }));
            break;

        case AuthMechanismIDs.AuthModeSelection:
            this._pendingEvents.push(new AuthdGdm.EventData({
                type: AuthdGdm.EventType.authModeSelected,
                authModeSelected: new AuthdGdm.Events.AuthModeSelected({
                    authModeId: key,
                }),
            }));
            break;
        }
    }

    cancelRequested() {
        if (!this._stage ||
            this._stage === AuthdPam.Stage.brokerSelection ||
            this._stage === AuthdPam.Stage.userSelection) {
            this.emit('reset');
            return true;
        }

        this._selectedAuthMode = null;
        this._requestStageChange(this._stage - 1);
        return true;
    }

    _onStageChanged(stage) {
        if (this._stage !== stage) {
            this.emit('clear-message-queue');
            this._stage = stage;
        }

        switch (stage) {
        case AuthdPam.Stage.userSelection:
            this._cancelAndReset();
            break;

        case AuthdPam.Stage.brokerSelection:
            this._authenticationInProgress = false;
            this._uiLayout = null;
            this._maybeStartBrokerSelection();
            break;

        case AuthdPam.Stage.authModeSelection:
            this._authenticationInProgress = false;
            this._uiLayout = null;
            this._maybeStartAuthModeSelection();
            break;

        case AuthdPam.Stage.challenge:
            this._maybeStartChallenge();
            break;
        }
    }

    _handleAuthdEvent(event) {
        this.emit('service-request', event);

        switch (event.type) {
        case AuthdGdm.EventType.userSelected:
            break;

        case AuthdGdm.EventType.brokersReceived:
            this._brokers = {};
            event.brokersReceived.brokersInfos.forEach(brokerInfo => {
                this._brokers[brokerInfo.id] = brokerInfo.name;
            });
            this._maybeStartBrokerSelection();

            break;

        case AuthdGdm.EventType.brokerSelected:
            this._selectedBroker = event.brokerSelected.brokerId;
            if (this._stage === AuthdPam.Stage.brokerSelection)
                this.emit('choice-list-selected', this._selectedBroker);

            console.log('Broker selected', this._selectedBroker);
            break;

        case AuthdGdm.EventType.authModesReceived:
            this._authMechanisms = {};
            this._authModes = {};
            event.authModesReceived.authModes.forEach(authMode => {
                // if (Object.values(this._authMechanisms).length)
                //     return;
                this._authModes[authMode.id] = authMode.label;
                this._authMechanisms[authMode.id] = {
                    name: authMode.label,
                    selectable: true,
                    /* FIXME: we can't really define the real role until the mechanism is selected */
                    role: Const.PASSWORD_ROLE_NAME,
                    id: authMode.id,
                    prompt: authMode.label,
                    serviceName: SERVICE_NAME,
                    protocol: this.protocolName,
                };
            });

            // this.emit('mechanisms-changed', this._authMechanisms);
            this._maybeStartAuthModeSelection();
            break;

        case AuthdGdm.EventType.authModeSelected:
            this._selectedAuthMode = event.authModeSelected.authModeId;
            if (this._stage >= AuthdPam.Stage.brokerSelection)
                this._maybeStartChallenge();
            /*  else if (this._stage === AuthdPam.Stage.undefined) {
                    this._showAuthMode();
                } */
            break;

        case AuthdGdm.EventType.uiLayoutReceived:
            this._uiLayout = event.uiLayoutReceived.uiLayout;
            this._maybeStartChallenge();
            break;

        case AuthdGdm.EventType.startAuthentication:
            this._authenticationStarted = true;
            this._pendingNewChallenge = null;
            this._maybeStartChallenge();

            break;

        case AuthdGdm.EventType.authEvent:
            this._authenticationInProgress = false;

            if (!this._authenticationStarted)
                return;
            this._authenticationStarted = false;
            this._handleAuthResponse(event.authEvent.response);
            break;

        default:
            throw new Error(`Unhandled event type ${event.type}`);
        }
    }

    reset() {
        this.cancel();
    }

    cancel() {
        this._uiLayout = null;
        this._pendingNewChallenge = null;
        this._selectedAuthMode = null;
        this._authenticationInProgress = false;
        this._authenticationStarted = false;
        this._authMechanisms = {};
        this._authModes = {};
        this.emit('mechanisms-changed', {});
    }

    _maybeStartChallenge() {
        if (!this._selectedAuthMode)
            return;

        if (!this._authenticationStarted)
            return;

        if (this._authenticationInProgress)
            return;

        if (!this._uiLayout)
            return;

        if (!this._authModes[this._selectedAuthMode])
            return;

        this._showChallenge();
    }

    sortMechanisms(mA, mB) {
        if (mA.id === this._selectedAuthMode)
            return -1;
        if (mB.id === this._selectedAuthMode)
            return 1;
        return 0;
    }

    _showChallenge() {
        if (!this._selectedAuthMode)
            throw new Error('No authentication mode selected');
        if (!this._authModes[this._selectedAuthMode])
            throw new Error(`Selected authentication mode '${this._selectedAuthMode}' is not supported`);
        if (!this._authMechanisms[this._selectedAuthMode])
            throw new Error(`Selected authentication mode '${this._selectedAuthMode}' is not supported mechanism`);
        if (!this._uiLayout)
            throw new Error('No ui layouts defined');

        this._authenticationInProgress = true;
        this._stage = AuthdPam.Stage.challenge;

        const hasWait = this._uiLayout.wait === 'true';

        switch (this._uiLayout.type) {
        case UILayoutTypes.Form:
        case UILayoutTypes.NewPassword: {
            const label = this._uiLayout.label;
            let infoMsg;
            let prompt;
            const mechanism = this._authMechanisms[this._selectedAuthMode];
            if (hasWait)
                infoMsg = label;

            if (this._uiLayout.entry?.length) {
                prompt = label;

                if (!prompt?.length) {
                    switch (this._uiLayout.entry) {
                    case EntryTypes.CharsPassword:
                        prompt = _('Password');
                        break;
                    case EntryTypes.Chars:
                        prompt = _('Input value');
                        break;
                    case EntryTypes.DigitsPassword:
                        // FIXME: Add another role for validating them
                        prompt = _('Input numbers');
                        break;
                    case EntryTypes.Digits:
                        // FIXME: Add another role for validating them
                        prompt = _('Input numbers');
                        break;
                    }
                }

                if (!isEntryPassword(this._uiLayout.entry))
                    mechanism.role = Const.PLAIN_TEXT_ROLE_NAME;
            } else {
                // FIXME: We need to add other roles to show wait actions.
                // mechanism.role = Const.PLAIN_TEXT_ROLE_NAME;
                // delete mechanism.role;
                mechanism.role = Const.MESSAGE_ROLE_NAME;
                prompt = infoMsg;
            }

            mechanism.prompt = prompt ?? '';
            this.emit('mechanisms-changed', this._authMechanisms);
            this.emit('start-mechanism', mechanism);

            if (infoMsg?.length && mechanism.prompt !== infoMsg)
                this.emit('queue-message', infoMsg, MessageType.INFO);

            if (!hasWait)
                break;

            this._pendingEvents.push(new AuthdGdm.EventData({
                type: AuthdGdm.EventType.isAuthenticatedRequested,
                isAuthenticatedRequested: new AuthdGdm.Events.IsAuthenticatedRequested({
                    authenticationData: new Authd.IARequest.AuthenticationData({
                        wait: 'true',
                    }),
                }),
            }));
            break;
        }

        case UILayoutTypes.QrCode: {
            const mechanism = this._authMechanisms[this._selectedAuthMode];
            mechanism.role = Const.WEB_LOGIN_ROLE_NAME;
            mechanism.link_prompt = this._uiLayout.label;
            mechanism.init_prompt = _('Use the qr code to log-in');
            mechanism.uri = this._uiLayout.content;

            this.emit('mechanisms-changed', this._authMechanisms);
            this.emit('start-mechanism', mechanism);
            break;
        }

        default:
            throw new Error(`UI Layout ${this._uiLayout.type} is not handled`);
        }
    }

    _handleAuthResponse(response) {
        this._pendingNewChallenge = null;

        if (response.access !== AuthResult.Retry) {
            this._selectedAuthMode = null;
            this._uiLayout = null;
        }

        switch (response.access) {
        case AuthResult.Granted:
            this.emit('verification-complete');
            break;

        case AuthResult.Retry:
        case AuthResult.Denied: {
            if (response?.msg)
                this.emit('queue-message', response.msg, MessageType.ERROR);

            if (response.access === AuthResult.Denied) {
                if (response?.msg?.length) {
                    GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
                        this._cancelAndReset();
                        return false;
                    });
                } else {
                    this._cancelAndReset();
                }
            } else if (response.access === AuthResult.Retry) {
                this._maybeStartAuthModeSelection();
            }
            break;
        }

        case AuthResult.Cancelled: {
            this.emit('queue-message',
                _('Authentication was cancelled, try again!'), MessageType.ERROR);

            this._doStageChange(AuthdPam.Stage.authModeSelection);
            break;
        }

        case AuthResult.Next: {
            this.emit('clear-message-queue');
            this._authModes = {};
            break;
        }
        }
    }

    _cancelAndReset() {
        this.emit('cancel');
        this.emit('reset');
    }
}
(uuay)gdm/+*��s~eC(@U$�panel.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Animation from './animation.js';
import {AppMenu} from './appMenu.js';
import * as Config from '../misc/config.js';
import * as CtrlAltTab from './ctrlAltTab.js';
import * as DND from './dnd.js';
import * as Overview from './overview.js';
import * as PopupMenu from './popupMenu.js';
import * as PanelMenu from './panelMenu.js';
import {QuickSettingsMenu, SystemIndicator} from './quickSettings.js';
import * as Main from './main.js';
import * as Util from '../misc/util.js';

import * as RemoteAccessStatus from './status/remoteAccess.js';
import * as PowerProfileStatus from './status/powerProfiles.js';
import * as RFKillStatus from './status/rfkill.js';
import * as CameraStatus from './status/camera.js';
import * as VolumeStatus from './status/volume.js';
import * as BrightnessStatus from './status/brightness.js';
import * as SystemStatus from './status/system.js';
import * as LocationStatus from './status/location.js';
import * as NightLightStatus from './status/nightLight.js';
import * as DarkModeStatus from './status/darkMode.js';
import * as BacklightStatus from './status/backlight.js';
import * as ThunderboltStatus from './status/thunderbolt.js';
import * as AutoRotateStatus from './status/autoRotate.js';
import * as BackgroundAppsStatus from './status/backgroundApps.js';

import {DateMenuButton} from './dateMenu.js';
import {ATIndicator} from './status/accessibility.js';
import {InputSourceIndicator} from './status/keyboard.js';
import {DwellClickIndicator} from './status/dwellClick.js';
import {ScreenRecordingIndicator, ScreenSharingIndicator} from './status/remoteAccess.js';

const PANEL_ICON_SIZE = 16;
const APP_MENU_ICON_MARGIN = 0;

const BUTTON_DND_ACTIVATION_TIMEOUT = 250;

const N_QUICK_SETTINGS_COLUMNS = 2;

const INACTIVE_WORKSPACE_DOT_SCALE = 0.75;

/**
 * AppMenuButton:
 *
 * This class manages the "application menu" component.  It tracks the
 * currently focused application.  However, when an app is launched,
 * this menu also handles startup notification for it.  So when we
 * have an active startup notification, we switch modes to display that.
 */
const AppMenuButton = GObject.registerClass({
    Signals: {'changed': {}},
}, class AppMenuButton extends PanelMenu.Button {
    _init(panel) {
        super._init(0.0, null, true);

        this.accessible_role = Atk.Role.MENU;

        this._startingApps = [];

        this._menuManager = panel.menuManager;
        this._targetApp = null;

        let bin = new St.Bin({name: 'appMenu'});
        this.add_child(bin);

        this.bind_property('reactive', this, 'can-focus', 0);
        this.reactive = false;

        this._container = new St.BoxLayout({style_class: 'panel-status-menu-box'});
        bin.set_child(this._container);

        let textureCache = St.TextureCache.get_default();
        textureCache.connect('icon-theme-changed',
            this._onIconThemeChanged.bind(this));

        let iconEffect = new Clutter.DesaturateEffect();
        this._iconBox = new St.Bin({
            style_class: 'app-menu-icon',
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._iconBox.add_effect(iconEffect);
        this._container.add_child(this._iconBox);

        this._iconBox.connect('style-changed', () => {
            let themeNode = this._iconBox.get_theme_node();
            iconEffect.enabled = themeNode.get_icon_style() === St.IconStyle.SYMBOLIC;
        });

        this._label = new St.Label({
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._container.add_child(this._label);

        this._visible = !Main.overview.visible;
        if (!this._visible)
            this.hide();
        Main.overview.connectObject(
            'hiding', this._sync.bind(this),
            'showing', this._sync.bind(this), this);

        this._spinner = new Animation.Spinner(PANEL_ICON_SIZE, {
            animate: true,
            hideOnStop: true,
        });
        this._container.add_child(this._spinner);

        let menu = new AppMenu(this);
        this.setMenu(menu);
        this._menuManager.addMenu(menu);

        Shell.WindowTracker.get_default().connectObject('notify::focus-app',
            this._focusAppChanged.bind(this), this);
        Shell.AppSystem.get_default().connectObject('app-state-changed',
            this._onAppStateChanged.bind(this), this);
        global.window_manager.connectObject('switch-workspace',
            this._sync.bind(this), this);

        this._sync();
    }

    fadeIn() {
        if (this._visible)
            return;

        this._visible = true;
        this.reactive = true;
        this.remove_all_transitions();
        this.ease({
            opacity: 255,
            duration: Overview.ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    fadeOut() {
        if (!this._visible)
            return;

        this._visible = false;
        this.reactive = false;
        this.remove_all_transitions();
        this.ease({
            opacity: 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            duration: Overview.ANIMATION_TIME,
        });
    }

    _syncIcon(app) {
        const icon = app.create_icon_texture(PANEL_ICON_SIZE - APP_MENU_ICON_MARGIN);
        this._iconBox.set_child(icon);
    }

    _onIconThemeChanged() {
        if (this._iconBox.child == null)
            return;

        if (this._targetApp)
            this._syncIcon(this._targetApp);
    }

    stopAnimation() {
        this._spinner.stop();
    }

    startAnimation() {
        this._spinner.play();
    }

    _onAppStateChanged(appSys, app) {
        let state = app.state;
        if (state !== Shell.AppState.STARTING)
            this._startingApps = this._startingApps.filter(a => a !== app);
        else if (state === Shell.AppState.STARTING)
            this._startingApps.push(app);
        // For now just resync on all running state changes; this is mainly to handle
        // cases where the focused window's application changes without the focus
        // changing.  An example case is how we map OpenOffice.org based on the window
        // title which is a dynamic property.
        this._sync();
    }

    _focusAppChanged() {
        let tracker = Shell.WindowTracker.get_default();
        let focusedApp = tracker.focus_app;
        if (!focusedApp) {
            // If the app has just lost focus to the panel, pretend
            // nothing happened; otherwise you can't keynav to the
            // app menu.
            if (global.stage.key_focus != null)
                return;
        }
        this._sync();
    }

    _findTargetApp() {
        let workspaceManager = global.workspace_manager;
        let workspace = workspaceManager.get_active_workspace();
        let tracker = Shell.WindowTracker.get_default();
        let focusedApp = tracker.focus_app;
        if (focusedApp && focusedApp.is_on_workspace(workspace))
            return focusedApp;

        for (let i = 0; i < this._startingApps.length; i++) {
            if (this._startingApps[i].is_on_workspace(workspace))
                return this._startingApps[i];
        }

        return null;
    }

    _sync() {
        let targetApp = this._findTargetApp();

        if (this._targetApp !== targetApp) {
            this._targetApp?.disconnectObject(this);

            this._targetApp = targetApp;

            if (this._targetApp) {
                this._targetApp.connectObject('notify::busy', this._sync.bind(this), this);
                this._label.set_text(this._targetApp.get_name());
                this.set_accessible_name(this._targetApp.get_name());

                this._syncIcon(this._targetApp);
            }
        }

        let visible = this._targetApp != null && !Main.overview.visibleTarget;
        if (visible)
            this.fadeIn();
        else
            this.fadeOut();

        let isBusy = this._targetApp != null &&
                      (this._targetApp.get_state() === Shell.AppState.STARTING ||
                       this._targetApp.get_busy());
        if (isBusy)
            this.startAnimation();
        else
            this.stopAnimation();

        this.reactive = visible && !isBusy;

        this.menu.setApp(this._targetApp);
        this.emit('changed');
    }
});

const WorkspaceDot = GObject.registerClass({
    Properties: {
        'expansion': GObject.ParamSpec.double('expansion', '', '',
            GObject.ParamFlags.READWRITE,
            0.0, 1.0, 0.0),
        'width-multiplier': GObject.ParamSpec.double(
            'width-multiplier', '', '',
            GObject.ParamFlags.READWRITE,
            1.0, 10.0, 1.0),
    },
}, class WorkspaceDot extends Clutter.Actor {
    constructor(params = {}) {
        super({
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
            ...params,
        });

        this._dot = new St.Widget({
            style_class: 'workspace-dot',
            y_align: Clutter.ActorAlign.CENTER,
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
            request_mode: Clutter.RequestMode.WIDTH_FOR_HEIGHT,
        });
        this.add_child(this._dot);

        this.connect('notify::width-multiplier', () => this.queue_relayout());
        this.connect('notify::expansion', () => {
            this._updateVisuals();
            this.queue_relayout();
        });
        this._updateVisuals();

        this._destroying = false;
    }

    _updateVisuals() {
        const {expansion} = this;

        this._dot.set({
            opacity: Util.lerp(0.50, 1.0, expansion) * 255,
            scaleX: Util.lerp(INACTIVE_WORKSPACE_DOT_SCALE, 1.0, expansion),
            scaleY: Util.lerp(INACTIVE_WORKSPACE_DOT_SCALE, 1.0, expansion),
        });
    }

    vfunc_get_preferred_width(forHeight) {
        const factor = Util.lerp(1.0, this.widthMultiplier, this.expansion);
        return this._dot.get_preferred_width(forHeight).map(v => Math.round(v * factor));
    }

    vfunc_get_preferred_height(forWidth) {
        return this._dot.get_preferred_height(forWidth);
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        box.set_origin(0, 0);
        this._dot.allocate(box);
    }

    scaleIn() {
        this.set({
            scale_x: 0,
            scale_y: 0,
        });

        this.ease({
            duration: 500,
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            scale_x: 1.0,
            scale_y: 1.0,
        });
    }

    scaleOutAndDestroy() {
        this._destroying = true;

        this.ease({
            duration: 500,
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            scale_x: 0.0,
            scale_y: 0.0,
            onComplete: () => this.destroy(),
        });
    }

    get destroying() {
        return this._destroying;
    }
});

const WorkspaceIndicators = GObject.registerClass(
class WorkspaceIndicators extends St.BoxLayout {
    constructor() {
        super();

        this._workspacesAdjustment = Main.createWorkspacesAdjustment(this);
        this._workspacesAdjustment.connectObject(
            'notify::value', () => this._updateExpansion(),
            'notify::upper', () => this._recalculateDots(),
            this);

        for (let i = 0; i < this._workspacesAdjustment.upper; i++)
            this.insert_child_at_index(new WorkspaceDot(), i);
        this._updateExpansion();
    }

    _getActiveIndicators() {
        return [...this].filter(i => !i.destroying);
    }

    _recalculateDots() {
        const activeIndicators = this._getActiveIndicators();
        const nIndicators = activeIndicators.length;
        const targetIndicators = this._workspacesAdjustment.upper;

        let remaining = Math.abs(nIndicators - targetIndicators);
        while (remaining--) {
            if (nIndicators < targetIndicators) {
                const indicator = new WorkspaceDot();
                this.add_child(indicator);
                indicator.scaleIn();
            } else {
                const indicator = activeIndicators[nIndicators - remaining - 1];
                indicator.scaleOutAndDestroy();
            }
        }

        this._updateExpansion();
    }

    _updateExpansion() {
        const nIndicators = this._getActiveIndicators().length;
        const activeWorkspace = this._workspacesAdjustment.value;

        let widthMultiplier;
        if (nIndicators <= 2)
            widthMultiplier = 3.625;
        else if (nIndicators <= 5)
            widthMultiplier = 3.25;
        else
            widthMultiplier = 2.75;

        this.get_children().forEach((indicator, index) => {
            const distance = Math.abs(index - activeWorkspace);
            indicator.expansion = Math.clamp(1 - distance, 0, 1);
            indicator.widthMultiplier = widthMultiplier;
        });
    }
});

const ActivitiesButton = GObject.registerClass(
class ActivitiesButton extends PanelMenu.Button {
    _init() {
        super._init(0.0, null, true);

        this.set({
            name: 'panelActivities',
            accessible_role: Atk.Role.TOGGLE_BUTTON,
            /* Translators: If there is no suitable word for "Activities"
               in your language, you can use the word for "Overview". */
            accessible_name: _('Activities'),
        });

        this.add_child(new WorkspaceIndicators());

        Main.overview.connectObject('showing',
            () => this.add_style_pseudo_class('checked'),
            this);
        Main.overview.connectObject('hiding',
            () => this.remove_style_pseudo_class('checked'),
            this);

        this._xdndTimeOut = 0;
    }

    handleDragOver(source, _actor, _x, _y, _time) {
        if (source !== Main.xdndHandler)
            return DND.DragMotionResult.CONTINUE;

        if (this._xdndTimeOut !== 0)
            GLib.source_remove(this._xdndTimeOut);
        this._xdndTimeOut = GLib.timeout_add(GLib.PRIORITY_DEFAULT, BUTTON_DND_ACTIVATION_TIMEOUT, () => {
            this._xdndToggleOverview();
        });
        GLib.Source.set_name_by_id(this._xdndTimeOut, '[gnome-shell] this._xdndToggleOverview');

        return DND.DragMotionResult.CONTINUE;
    }

    vfunc_event(event) {
        if (event.type() === Clutter.EventType.TOUCH_END ||
            event.type() === Clutter.EventType.BUTTON_RELEASE) {
            if (Main.overview.shouldToggleByCornerOrButton())
                Main.overview.toggle();
        }

        return Main.wm.handleWorkspaceScroll(event);
    }

    vfunc_key_release_event(event) {
        let symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_Return || symbol === Clutter.KEY_space) {
            if (Main.overview.shouldToggleByCornerOrButton()) {
                Main.overview.toggle();
                return Clutter.EVENT_STOP;
            }
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _xdndToggleOverview() {
        let [x, y] = global.get_pointer();
        let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);

        if (pickedActor === this && Main.overview.shouldToggleByCornerOrButton())
            Main.overview.toggle();

        GLib.source_remove(this._xdndTimeOut);
        this._xdndTimeOut = 0;
        return GLib.SOURCE_REMOVE;
    }
});

const UnsafeModeIndicator = GObject.registerClass(
class UnsafeModeIndicator extends SystemIndicator {
    _init() {
        super._init();

        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'channel-insecure-symbolic';

        global.context.bind_property('unsafe-mode',
            this._indicator, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
    }
});

const QuickSettings = GObject.registerClass(
class QuickSettings extends PanelMenu.Button {
    constructor() {
        super(0.0, C_('System menu in the top bar', 'System'), true);

        this._indicators = new St.BoxLayout({
            style_class: 'panel-status-indicators-box',
        });
        this.add_child(this._indicators);

        this.setMenu(new QuickSettingsMenu(this, N_QUICK_SETTINGS_COLUMNS));

        this._setupIndicators().catch(error =>
            logError(error, 'Failed to setup quick settings'));
    }

    async _setupIndicators() {
        if (Config.HAVE_NETWORKMANAGER) {
            /** @type {import('./status/network.js')} */
            const NetworkStatus = await import('./status/network.js');

            this._network = new NetworkStatus.Indicator();
        } else {
            this._network = null;
        }

        if (Config.HAVE_BLUETOOTH) {
            /** @type {import('./status/bluetooth.js')} */
            const BluetoothStatus = await import('./status/bluetooth.js');

            this._bluetooth = new BluetoothStatus.Indicator();
        } else {
            this._bluetooth = null;
        }

        this._system = new SystemStatus.Indicator();
        this._camera = new CameraStatus.Indicator();
        this._volumeOutput = new VolumeStatus.OutputIndicator();
        this._volumeInput = new VolumeStatus.InputIndicator();
        this._brightness = new BrightnessStatus.Indicator();
        this._remoteAccess = new RemoteAccessStatus.RemoteAccessApplet();
        this._location = new LocationStatus.Indicator();
        this._thunderbolt = new ThunderboltStatus.Indicator();
        this._nightLight = new NightLightStatus.Indicator();
        this._darkMode = new DarkModeStatus.Indicator();
        this._backlight = new BacklightStatus.Indicator();
        this._powerProfiles = new PowerProfileStatus.Indicator();
        this._rfkill = new RFKillStatus.Indicator();
        this._autoRotate = new AutoRotateStatus.Indicator();
        this._unsafeMode = new UnsafeModeIndicator();
        this._backgroundApps = new BackgroundAppsStatus.Indicator();

        // add privacy-related indicators before any external indicators
        let pos = 0;
        this._indicators.insert_child_at_index(this._remoteAccess, pos++);
        this._indicators.insert_child_at_index(this._camera, pos++);
        this._indicators.insert_child_at_index(this._volumeInput, pos++);
        this._indicators.insert_child_at_index(this._location, pos++);

        // append all other indicators
        this._indicators.add_child(this._brightness);
        this._indicators.add_child(this._thunderbolt);
        this._indicators.add_child(this._nightLight);
        if (this._network)
            this._indicators.add_child(this._network);
        this._indicators.add_child(this._darkMode);
        this._indicators.add_child(this._backlight);
        this._indicators.add_child(this._powerProfiles);
        if (this._bluetooth)
            this._indicators.add_child(this._bluetooth);
        this._indicators.add_child(this._rfkill);
        this._indicators.add_child(this._autoRotate);
        this._indicators.add_child(this._volumeOutput);
        this._indicators.add_child(this._unsafeMode);
        this._indicators.add_child(this._system);

        // add our quick settings items before any external ones
        const sibling = this.menu.getFirstItem();
        this._addItemsBefore(this._system.quickSettingsItems,
            sibling, N_QUICK_SETTINGS_COLUMNS);
        this._addItemsBefore(this._volumeOutput.quickSettingsItems,
            sibling, N_QUICK_SETTINGS_COLUMNS);
        this._addItemsBefore(this._volumeInput.quickSettingsItems,
            sibling, N_QUICK_SETTINGS_COLUMNS);
        this._addItemsBefore(this._brightness.quickSettingsItems,
            sibling, N_QUICK_SETTINGS_COLUMNS);

        this._addItemsBefore(this._camera.quickSettingsItems, sibling);
        this._addItemsBefore(this._remoteAccess.quickSettingsItems, sibling);
        this._addItemsBefore(this._thunderbolt.quickSettingsItems, sibling);
        this._addItemsBefore(this._location.quickSettingsItems, sibling);
        if (this._network)
            this._addItemsBefore(this._network.quickSettingsItems, sibling);
        if (this._bluetooth)
            this._addItemsBefore(this._bluetooth.quickSettingsItems, sibling);
        this._addItemsBefore(this._powerProfiles.quickSettingsItems, sibling);
        this._addItemsBefore(this._nightLight.quickSettingsItems, sibling);
        this._addItemsBefore(this._darkMode.quickSettingsItems, sibling);
        this._addItemsBefore(this._backlight.quickSettingsItems, sibling);
        this._addItemsBefore(this._rfkill.quickSettingsItems, sibling);
        this._addItemsBefore(this._autoRotate.quickSettingsItems, sibling);
        this._addItemsBefore(this._unsafeMode.quickSettingsItems, sibling);

        // append background apps
        this._backgroundApps.quickSettingsItems.forEach(
            item => this.menu.addItem(item, N_QUICK_SETTINGS_COLUMNS));
    }

    _addItemsBefore(items, sibling, colSpan = 1) {
        items.forEach(item => this.menu.insertItemBefore(item, sibling, colSpan));
    }

    /**
     * Insert indicator and quick settings items at
     * appropriate positions
     *
     * @param {PanelMenu.Button} indicator
     * @param {number=} colSpan
     */
    addExternalIndicator(indicator, colSpan = 1) {
        // Insert before first non-privacy indicator if it exists
        let sibling = this._brightness ?? null;
        this._indicators.insert_child_below(indicator, sibling);

        // Insert before background apps if it exists
        sibling = this._backgroundApps?.quickSettingsItems?.at(-1) ?? null;
        this._addItemsBefore(indicator.quickSettingsItems, sibling, colSpan);
    }
});

const PANEL_ITEM_IMPLEMENTATIONS = {
    'activities': ActivitiesButton,
    'appMenu': AppMenuButton,
    'quickSettings': QuickSettings,
    'dateMenu': DateMenuButton,
    'a11y': ATIndicator,
    'keyboard': InputSourceIndicator,
    'dwellClick': DwellClickIndicator,
    'screenRecording': ScreenRecordingIndicator,
    'screenSharing': ScreenSharingIndicator,
};

export const Panel = GObject.registerClass(
class Panel extends St.Widget {
    _init() {
        super._init({
            name: 'panel',
            reactive: true,
        });

        this.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);

        this._sessionStyle = null;

        this.statusArea = {};

        this.menuManager = new PopupMenu.PopupMenuManager(this);

        this._leftBox = new St.BoxLayout({name: 'panelLeft'});
        this.add_child(this._leftBox);
        this._centerBox = new St.BoxLayout({name: 'panelCenter'});
        this.add_child(this._centerBox);
        this._rightBox = new St.BoxLayout({name: 'panelRight'});
        this.add_child(this._rightBox);

        this.connect('button-press-event', this._onButtonPress.bind(this));
        this.connect('touch-event', this._onTouchEvent.bind(this));

        Main.overview.connectObject('showing',
            () => this.add_style_pseudo_class('overview'),
            this);
        Main.overview.connectObject('hiding',
            () => this.remove_style_pseudo_class('overview'),
            this);

        Main.layoutManager.panelBox.add_child(this);
        Main.ctrlAltTabManager.addGroup(this,
            _('Top Bar'), 'shell-focus-top-bar-symbolic',
            {sortGroup: CtrlAltTab.SortGroup.TOP});

        Main.sessionMode.connect('updated', this._updatePanel.bind(this));

        global.display.connect('workareas-changed', () => this.queue_relayout());
        this._updatePanel();
    }

    vfunc_get_preferred_width(_forHeight) {
        let primaryMonitor = Main.layoutManager.primaryMonitor;

        if (primaryMonitor)
            return [0, primaryMonitor.width];

        return [0,  0];
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        let allocWidth = box.x2 - box.x1;
        let allocHeight = box.y2 - box.y1;

        let [, leftNaturalWidth] = this._leftBox.get_preferred_width(-1);
        let [, centerNaturalWidth] = this._centerBox.get_preferred_width(-1);
        let [, rightNaturalWidth] = this._rightBox.get_preferred_width(-1);

        let sideWidth, centerWidth;
        centerWidth = centerNaturalWidth;

        // get workspace area and center date entry relative to it
        let monitor = Main.layoutManager.findMonitorForActor(this);
        let centerOffset = 0;
        if (monitor) {
            let workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index);
            centerOffset = 2 * (workArea.x - monitor.x) + workArea.width - monitor.width;
        }

        sideWidth = Math.max(0, (allocWidth - centerWidth + centerOffset) / 2);

        let childBox = new Clutter.ActorBox();

        childBox.y1 = 0;
        childBox.y2 = allocHeight;
        if (this.get_text_direction() === Clutter.TextDirection.RTL) {
            childBox.x1 = Math.max(
                allocWidth - Math.min(Math.floor(sideWidth), leftNaturalWidth),
                0);
            childBox.x2 = allocWidth;
        } else {
            childBox.x1 = 0;
            childBox.x2 = Math.min(Math.floor(sideWidth), leftNaturalWidth);
        }
        this._leftBox.allocate(childBox);

        childBox.x1 = Math.ceil(sideWidth);
        childBox.y1 = 0;
        childBox.x2 = childBox.x1 + centerWidth;
        childBox.y2 = allocHeight;
        this._centerBox.allocate(childBox);

        childBox.y1 = 0;
        childBox.y2 = allocHeight;
        if (this.get_text_direction() === Clutter.TextDirection.RTL) {
            childBox.x1 = 0;
            childBox.x2 = Math.min(Math.floor(sideWidth), rightNaturalWidth);
        } else {
            childBox.x1 = Math.max(
                allocWidth - Math.min(Math.floor(sideWidth), rightNaturalWidth),
                0);
            childBox.x2 = allocWidth;
        }
        this._rightBox.allocate(childBox);
    }

    _tryDragWindow(event) {
        if (Main.modalCount > 0)
            return Clutter.EVENT_PROPAGATE;

        const targetActor = global.stage.get_event_actor(event);
        if (targetActor !== this)
            return Clutter.EVENT_PROPAGATE;

        const [x, y] = event.get_coords();
        let dragWindow = this._getDraggableWindowForPosition(x);

        if (!dragWindow)
            return Clutter.EVENT_PROPAGATE;

        const positionHint = new Graphene.Point({x, y});
        return dragWindow.begin_grab_op(
            Meta.GrabOp.MOVING,
            event.get_device(),
            event.get_event_sequence(),
            event.get_time(),
            positionHint) ? Clutter.EVENT_STOP : Clutter.EVENT_PROPAGATE;
    }

    _onButtonPress(actor, event) {
        if (event.get_button() !== Clutter.BUTTON_PRIMARY)
            return Clutter.EVENT_PROPAGATE;

        return this._tryDragWindow(event);
    }

    _onTouchEvent(actor, event) {
        if (event.type() !== Clutter.EventType.TOUCH_BEGIN)
            return Clutter.EVENT_PROPAGATE;

        return this._tryDragWindow(event);
    }

    vfunc_key_press_event(event) {
        let symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_Escape) {
            global.display.focus_default_window(event.get_time());
            return Clutter.EVENT_STOP;
        }

        return super.vfunc_key_press_event(event);
    }

    _toggleMenu(indicator) {
        if (!indicator || !indicator.mapped)
            return; // menu not supported by current session mode

        let menu = indicator.menu;
        if (!indicator.reactive)
            return;

        menu.toggle();
        if (menu.isOpen)
            menu.actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
    }

    _closeMenu(indicator) {
        if (!indicator || !indicator.mapped)
            return; // menu not supported by current session mode

        if (!indicator.reactive)
            return;

        indicator.menu.close();
    }

    toggleCalendar() {
        this._toggleMenu(this.statusArea.dateMenu);
    }

    toggleQuickSettings() {
        this._toggleMenu(this.statusArea.quickSettings);
    }

    closeCalendar() {
        this._closeMenu(this.statusArea.dateMenu);
    }

    closeQuickSettings() {
        this._closeMenu(this.statusArea.quickSettings);
    }

    set boxOpacity(value) {
        let isReactive = value > 0;

        this._leftBox.opacity = value;
        this._leftBox.reactive = isReactive;
        this._centerBox.opacity = value;
        this._centerBox.reactive = isReactive;
        this._rightBox.opacity = value;
        this._rightBox.reactive = isReactive;
    }

    get boxOpacity() {
        return this._leftBox.opacity;
    }

    _updatePanel() {
        let panel = Main.sessionMode.panel;
        this._hideIndicators();
        this._updateBox(panel.left, this._leftBox);
        this._updateBox(panel.center, this._centerBox);
        this._updateBox(panel.right, this._rightBox);

        if (panel.left.includes('dateMenu'))
            Main.messageTray.bannerAlignment = Clutter.ActorAlign.START;
        else if (panel.right.includes('dateMenu'))
            Main.messageTray.bannerAlignment = Clutter.ActorAlign.END;
        // Default to center if there is no dateMenu
        else
            Main.messageTray.bannerAlignment = Clutter.ActorAlign.CENTER;

        if (this._sessionStyle)
            this.remove_style_class_name(this._sessionStyle);

        this._sessionStyle = Main.sessionMode.panelStyle;
        if (this._sessionStyle)
            this.add_style_class_name(this._sessionStyle);
    }

    _hideIndicators() {
        for (let role in PANEL_ITEM_IMPLEMENTATIONS) {
            let indicator = this.statusArea[role];
            if (!indicator)
                continue;
            indicator.container.hide();
        }
    }

    _ensureIndicator(role) {
        let indicator = this.statusArea[role];
        if (!indicator) {
            let constructor = PANEL_ITEM_IMPLEMENTATIONS[role];
            if (!constructor) {
                // This icon is not implemented (this is a bug)
                return null;
            }
            indicator = new constructor(this);
            this.statusArea[role] = indicator;
        }
        return indicator;
    }

    _updateBox(elements, box) {
        let nChildren = box.get_n_children();

        for (let i = 0; i < elements.length; i++) {
            let role = elements[i];
            let indicator = this._ensureIndicator(role);
            if (indicator == null)
                continue;

            this._addToPanelBox(role, indicator, i + nChildren, box);
        }
    }

    _addToPanelBox(role, indicator, position, box) {
        let container = indicator.container;
        container.show();

        let parent = container.get_parent();
        if (parent)
            parent.remove_child(container);


        box.insert_child_at_index(container, position);
        this.statusArea[role] = indicator;
        let destroyId = indicator.connect('destroy', emitter => {
            delete this.statusArea[role];
            emitter.disconnect(destroyId);
        });
        indicator.connect('menu-set', this._onMenuSet.bind(this));
        this._onMenuSet(indicator);
    }

    addToStatusArea(role, indicator, position, box) {
        if (this.statusArea[role])
            throw new Error(`Extension point conflict: there is already a status indicator for role ${role}`);

        if (!(indicator instanceof PanelMenu.Button))
            throw new TypeError('Status indicator must be an instance of PanelMenu.Button');

        position ??= 0;
        let boxes = {
            left: this._leftBox,
            center: this._centerBox,
            right: this._rightBox,
        };
        let boxContainer = boxes[box] || this._rightBox;
        this.statusArea[role] = indicator;
        this._addToPanelBox(role, indicator, position, boxContainer);
        return indicator;
    }

    _onMenuSet(indicator) {
        if (!indicator.menu || indicator.menu._openChangedConnected)
            return;

        this.menuManager.addMenu(indicator.menu);

        indicator.menu._openChangedConnected = true;
        indicator.menu.connectObject('open-state-changed',
            (menu, isOpen) => {
                let boxAlignment;
                if (this._leftBox.contains(indicator.container))
                    boxAlignment = Clutter.ActorAlign.START;
                else if (this._centerBox.contains(indicator.container))
                    boxAlignment = Clutter.ActorAlign.CENTER;
                else if (this._rightBox.contains(indicator.container))
                    boxAlignment = Clutter.ActorAlign.END;

                if (boxAlignment === Main.messageTray.bannerAlignment)
                    Main.messageTray.bannerBlocked = isOpen;
            }, this);
    }

    _getDraggableWindowForPosition(stageX) {
        let workspaceManager = global.workspace_manager;
        const windows = workspaceManager.get_active_workspace().list_windows();
        const allWindowsByStacking =
            global.display.sort_windows_by_stacking(windows).reverse();

        return allWindowsByStacking.find(metaWindow => {
            let rect = metaWindow.get_frame_rect();
            return metaWindow.is_on_primary_monitor() &&
                   metaWindow.showing_on_its_workspace() &&
                   metaWindow.get_window_type() !== Meta.WindowType.DESKTOP &&
                   metaWindow.maximized_vertically &&
                   stageX > rect.x && stageX < rect.x + rect.width;
        });
    }
});
(uuay)layout.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Mtk from 'gi://Mtk';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';

import * as Background from './background.js';
import * as BackgroundMenu from './backgroundMenu.js';

import * as DND from './dnd.js';
import * as Main from './main.js';
import * as Params from '../misc/params.js';
import * as Ripples from './ripples.js';

const STARTUP_ANIMATION_TIME = 500;
const BACKGROUND_FADE_ANIMATION_TIME = 1000;

const HOT_CORNER_PRESSURE_THRESHOLD = 100; // pixels
const HOT_CORNER_PRESSURE_TIMEOUT = 1000; // ms

const SCREEN_TRANSITION_DELAY = 250; // ms
const SCREEN_TRANSITION_DURATION = 500; // ms

function isPopupMetaWindow(actor) {
    switch (actor.meta_window.get_window_type()) {
    case Meta.WindowType.DROPDOWN_MENU:
    case Meta.WindowType.POPUP_MENU:
    case Meta.WindowType.COMBO:
        return true;
    default:
        return false;
    }
}

export const MonitorConstraint = GObject.registerClass({
    Properties: {
        'primary': GObject.ParamSpec.boolean(
            'primary', 'Primary', 'Track primary monitor',
            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
            false),
        'index': GObject.ParamSpec.int(
            'index', 'Monitor index', 'Track specific monitor',
            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
            -1, 64, -1),
        'work-area': GObject.ParamSpec.boolean(
            'work-area', 'Work-area', 'Track monitor\'s work-area',
            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
            false),
    },
}, class MonitorConstraint extends Clutter.Constraint {
    _init(props) {
        this._primary = false;
        this._index = -1;
        this._workArea = false;

        super._init(props);
    }

    get primary() {
        return this._primary;
    }

    set primary(v) {
        if (v)
            this._index = -1;
        this._primary = v;
        if (this.actor)
            this.actor.queue_relayout();
        this.notify('primary');
    }

    get index() {
        return this._index;
    }

    set index(v) {
        this._primary = false;
        this._index = v;
        if (this.actor)
            this.actor.queue_relayout();
        this.notify('index');
    }

    get workArea() {
        return this._workArea;
    }

    set workArea(v) {
        if (v === this._workArea)
            return;
        this._workArea = v;
        if (this.actor)
            this.actor.queue_relayout();
        this.notify('work-area');
    }

    vfunc_set_actor(actor) {
        if (actor) {
            if (!this._monitorsChangedId) {
                this._monitorsChangedId =
                    Main.layoutManager.connect('monitors-changed', () => {
                        this.actor.queue_relayout();
                    });
            }

            if (!this._workareasChangedId) {
                this._workareasChangedId =
                    global.display.connect('workareas-changed', () => {
                        if (this._workArea)
                            this.actor.queue_relayout();
                    });
            }
        } else {
            if (this._monitorsChangedId)
                Main.layoutManager.disconnect(this._monitorsChangedId);
            this._monitorsChangedId = 0;

            if (this._workareasChangedId)
                global.display.disconnect(this._workareasChangedId);
            this._workareasChangedId = 0;
        }

        super.vfunc_set_actor(actor);
    }

    vfunc_update_allocation(actor, actorBox) {
        if (!this._primary && this._index < 0)
            return;

        if (!Main.layoutManager.primaryMonitor)
            return;

        let index;
        if (this._primary)
            index = Main.layoutManager.primaryIndex;
        else
            index = Math.min(this._index, Main.layoutManager.monitors.length - 1);

        let rect;
        if (this._workArea) {
            let workspaceManager = global.workspace_manager;
            let ws = workspaceManager.get_workspace_by_index(0);
            rect = ws.get_work_area_for_monitor(index);
        } else {
            rect = Main.layoutManager.monitors[index];
        }

        actorBox.init_rect(rect.x, rect.y, rect.width, rect.height);
    }
});

class Monitor {
    constructor(index, geometry, geometryScale) {
        this.index = index;
        this.x = geometry.x;
        this.y = geometry.y;
        this.width = geometry.width;
        this.height = geometry.height;
        this.geometry_scale = geometryScale;
    }

    get inFullscreen() {
        return global.display.get_monitor_in_fullscreen(this.index);
    }
}

const UiActor = GObject.registerClass(
class UiActor extends St.Widget {
    vfunc_get_preferred_width(_forHeight) {
        let width = global.stage.width;
        return [width, width];
    }

    vfunc_get_preferred_height(_forWidth) {
        let height = global.stage.height;
        return [height, height];
    }
});

const defaultParams = {
    trackFullscreen: false,
    affectsStruts: false,
    affectsInputRegion: true,
};

export const LayoutManager = GObject.registerClass({
    Signals: {
        'hot-corners-changed': {},
        'startup-complete': {},
        'startup-prepared': {},
        'monitors-changed': {},
        'system-modal-opened': {},
    },
}, class LayoutManager extends GObject.Object {
    _init() {
        super._init();

        this._rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
        this.monitors = [];
        this.primaryMonitor = null;
        this.primaryIndex = -1;
        this.hotCorners = [];

        this._keyboardIndex = -1;
        this._rightPanelBarrier = null;

        this._inOverview = false;
        this._updateRegionIdle = 0;

        this._trackedActors = [];
        this._topActors = [];
        this._isPopupWindowVisible = false;
        this._startingUp = true;
        this._pendingLoadBackground = false;

        // Set up stage hierarchy to group all UI actors under one container.
        this.uiGroup = new UiActor({name: 'uiGroup'});
        this.uiGroup.set_flags(Clutter.ActorFlags.NO_LAYOUT);

        global.stage.add_child(this.uiGroup);

        global.stage.remove_child(global.window_group);
        this.uiGroup.add_child(global.window_group);
        global.connect('shutdown', () => {
            const monitorManager = global.backend.get_monitor_manager();
            monitorManager.disconnectObject(this);

            const adoptedUiGroupActors = [
                global.window_group,
                global.top_window_group,
                global.compositor.get_feedback_group(),
            ];

            for (let adoptedActor of adoptedUiGroupActors) {
                this.uiGroup.remove_child(adoptedActor);
                global.stage.add_child(adoptedActor);
            }

            this._destroyHotCorners();
            this._destroyPanelBarrier();
            this.uiGroup.destroy();
        });

        // Using addChrome() to add actors to uiGroup will position actors
        // underneath the top_window_group.
        // To insert actors at the top of uiGroup, we use addTopChrome() or
        // add the actor directly using uiGroup.add_child().
        global.stage.remove_child(global.top_window_group);
        this.uiGroup.add_child(global.top_window_group);

        this.overviewGroup = new St.Widget({
            name: 'overviewGroup',
            visible: false,
            reactive: true,
            constraints: new Clutter.BindConstraint({
                source: this.uiGroup,
                coordinate: Clutter.BindCoordinate.ALL,
            }),
        });
        this.addChrome(this.overviewGroup);

        this.screenShieldGroup = new St.Widget({
            name: 'screenShieldGroup',
            visible: false,
            clip_to_allocation: true,
            layout_manager: new Clutter.BinLayout(),
            constraints: new Clutter.BindConstraint({
                source: this.uiGroup,
                coordinate: Clutter.BindCoordinate.ALL,
            }),
        });
        this.addChrome(this.screenShieldGroup);

        this.panelBox = new St.BoxLayout({
            name: 'panelBox',
            vertical: true,
        });
        this.addChrome(this.panelBox, {
            affectsStruts: true,
            trackFullscreen: true,
        });
        this.panelBox.connect('notify::allocation',
            this._panelBoxChanged.bind(this));

        this.modalDialogGroup = new St.Widget({
            name: 'modalDialogGroup',
            layout_manager: new Clutter.BinLayout(),
        });
        this.uiGroup.add_child(this.modalDialogGroup);

        this.keyboardBox = new St.BoxLayout({
            name: 'keyboardBox',
            reactive: true,
            track_hover: true,
        });
        this.addTopChrome(this.keyboardBox);
        this._keyboardHeightNotifyId = 0;

        this.screenshotUIGroup = new St.Widget({
            name: 'screenshotUIGroup',
            layout_manager: new Clutter.BinLayout(),
        });
        this.addTopChrome(this.screenshotUIGroup);

        // A dummy actor that tracks the mouse or text cursor, based on the
        // position and size set in setDummyCursorGeometry.
        this.dummyCursor = new St.Widget({width: 0, height: 0, opacity: 0});
        this.uiGroup.add_child(this.dummyCursor);

        const feedbackGroup = global.compositor.get_feedback_group();
        global.stage.remove_child(feedbackGroup);
        this.uiGroup.add_child(feedbackGroup);

        this._backgroundGroup = new Meta.BackgroundGroup();
        global.window_group.add_child(this._backgroundGroup);
        global.window_group.set_child_below_sibling(this._backgroundGroup, null);
        this._bgManagers = [];

        this._interfaceSettings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.interface',
        });

        this._interfaceSettings.connect('changed::enable-hot-corners',
            this._updateHotCorners.bind(this));

        // Need to update struts on new workspaces when they are added
        let workspaceManager = global.workspace_manager;
        workspaceManager.connect('notify::n-workspaces',
            this._queueUpdateRegions.bind(this));

        let display = global.display;
        display.connect('restacked',
            this._windowsRestacked.bind(this));
        display.connect('in-fullscreen-changed',
            this._updateFullscreen.bind(this));

        const monitorManager = global.backend.get_monitor_manager();
        monitorManager.connectObject(
            'monitors-changed', this._monitorsChanged.bind(this),
            this);
        this._monitorsChanged();

        this.screenTransition = new ScreenTransition();
        this.uiGroup.add_child(this.screenTransition);
        this.screenTransition.add_constraint(new Clutter.BindConstraint({
            source: this.uiGroup,
            coordinate: Clutter.BindCoordinate.ALL,
        }));
    }

    // This is called by Main after everything else is constructed
    init() {
        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));

        this._loadBackground();
    }

    showOverview() {
        this.overviewGroup.show();
        this.screenTransition.hide();

        this._inOverview = true;
        this._updateVisibility();
    }

    hideOverview() {
        this.overviewGroup.hide();
        this.screenTransition.hide();

        this._inOverview = false;
        this._updateVisibility();
    }

    _sessionUpdated() {
        this._updateVisibility();
        this._queueUpdateRegions();
    }

    _updateMonitors() {
        let display = global.display;

        this.monitors = [];
        let nMonitors = display.get_n_monitors();
        for (let i = 0; i < nMonitors; i++) {
            this.monitors.push(new Monitor(i,
                display.get_monitor_geometry(i),
                display.get_monitor_scale(i)));
        }

        if (nMonitors === 0) {
            this.primaryIndex = this.bottomIndex = -1;
        } else if (nMonitors === 1) {
            this.primaryIndex = this.bottomIndex = 0;
        } else {
            // If there are monitors below the primary, then we need
            // to split primary from bottom.
            this.primaryIndex = this.bottomIndex = display.get_primary_monitor();
            for (let i = 0; i < this.monitors.length; i++) {
                let monitor = this.monitors[i];
                if (this._isAboveOrBelowPrimary(monitor)) {
                    if (monitor.y > this.monitors[this.bottomIndex].y)
                        this.bottomIndex = i;
                }
            }
        }
        if (this.primaryIndex !== -1) {
            this.primaryMonitor = this.monitors[this.primaryIndex];
            this.bottomMonitor = this.monitors[this.bottomIndex];

            if (this._pendingLoadBackground) {
                this._loadBackground();
                this._pendingLoadBackground = false;
            }
        } else {
            this.primaryMonitor = null;
            this.bottomMonitor = null;
        }
    }

    _destroyHotCorners() {
        this.hotCorners.forEach(corner => corner?.destroy());
        this.hotCorners = [];
    }

    _updateHotCorners() {
        // destroy old hot corners
        this._destroyHotCorners();

        if (!this._interfaceSettings.get_boolean('enable-hot-corners')) {
            this.emit('hot-corners-changed');
            return;
        }

        let size = this.panelBox.height;

        // build new hot corners
        for (let i = 0; i < this.monitors.length; i++) {
            let monitor = this.monitors[i];
            let cornerX = this._rtl ? monitor.x + monitor.width : monitor.x;
            let cornerY = monitor.y;

            let haveTopLeftCorner = true;

            if (i !== this.primaryIndex) {
                // Check if we have a top left (right for RTL) corner.
                // I.e. if there is no monitor directly above or to the left(right)
                let besideX = this._rtl ? monitor.x + 1 : cornerX - 1;
                let besideY = cornerY;
                let aboveX = cornerX;
                let aboveY = cornerY - 1;

                for (let j = 0; j < this.monitors.length; j++) {
                    if (i === j)
                        continue;
                    let otherMonitor = this.monitors[j];
                    if (besideX >= otherMonitor.x &&
                        besideX < otherMonitor.x + otherMonitor.width &&
                        besideY >= otherMonitor.y &&
                        besideY < otherMonitor.y + otherMonitor.height) {
                        haveTopLeftCorner = false;
                        break;
                    }
                    if (aboveX >= otherMonitor.x &&
                        aboveX < otherMonitor.x + otherMonitor.width &&
                        aboveY >= otherMonitor.y &&
                        aboveY < otherMonitor.y + otherMonitor.height) {
                        haveTopLeftCorner = false;
                        break;
                    }
                }
            }

            if (haveTopLeftCorner) {
                let corner = new HotCorner(this, monitor, cornerX, cornerY);
                corner.setBarrierSize(size);
                this.hotCorners.push(corner);
            } else {
                this.hotCorners.push(null);
            }
        }

        this.emit('hot-corners-changed');
    }

    _addBackgroundMenu(bgManager) {
        BackgroundMenu.addBackgroundMenu(bgManager.backgroundActor, this);
    }

    _createBackgroundManager(monitorIndex) {
        const bgManager = new Background.BackgroundManager({
            container: this._backgroundGroup,
            layoutManager: this,
            monitorIndex,
        });

        bgManager.connect('changed', this._addBackgroundMenu.bind(this));
        this._addBackgroundMenu(bgManager);

        return bgManager;
    }

    _showSecondaryBackgrounds() {
        for (let i = 0; i < this.monitors.length; i++) {
            if (i !== this.primaryIndex) {
                let backgroundActor = this._bgManagers[i].backgroundActor;
                backgroundActor.show();
                backgroundActor.opacity = 0;
                backgroundActor.ease({
                    opacity: 255,
                    duration: BACKGROUND_FADE_ANIMATION_TIME,
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                });
            }
        }
    }

    _waitLoaded(bgManager) {
        return new Promise(resolve => {
            const id = bgManager.connect('loaded', () => {
                bgManager.disconnect(id);
                resolve();
            });
        });
    }

    _updateBackgrounds() {
        for (let i = 0; i < this._bgManagers.length; i++)
            this._bgManagers[i].destroy();

        this._bgManagers = [];

        if (Main.sessionMode.isGreeter)
            return Promise.resolve();

        for (let i = 0; i < this.monitors.length; i++) {
            let bgManager = this._createBackgroundManager(i);
            this._bgManagers.push(bgManager);

            if (i !== this.primaryIndex && this._startingUp)
                bgManager.backgroundActor.hide();
        }

        return Promise.all(this._bgManagers.map(this._waitLoaded));
    }

    _updateKeyboardBox() {
        this.keyboardBox.set_position(
            this.keyboardMonitor.x,
            this.keyboardMonitor.y + this.keyboardMonitor.height);
        this.keyboardBox.set_size(this.keyboardMonitor.width, -1);
    }

    _updateBoxes() {
        if (!this.primaryMonitor)
            return;

        this.panelBox.set_position(this.primaryMonitor.x, this.primaryMonitor.y);
        this.panelBox.set_size(this.primaryMonitor.width, -1);

        this.keyboardIndex = this.primaryIndex;
    }

    _panelBoxChanged() {
        this._updatePanelBarrier();

        let size = this.panelBox.height;
        this.hotCorners.forEach(corner => {
            if (corner)
                corner.setBarrierSize(size);
        });
    }

    _destroyPanelBarrier() {
        if (this._rightPanelBarrier) {
            this._rightPanelBarrier.destroy();
            this._rightPanelBarrier = null;
        }
    }

    _updatePanelBarrier() {
        this._destroyPanelBarrier();

        if (!this.primaryMonitor)
            return;

        if (this.panelBox.height) {
            let primary = this.primaryMonitor;

            this._rightPanelBarrier = new Meta.Barrier({
                backend: global.backend,
                x1: primary.x + primary.width, y1: primary.y,
                x2: primary.x + primary.width, y2: primary.y + this.panelBox.height,
                directions: Meta.BarrierDirection.NEGATIVE_X,
            });
        }
    }

    _monitorsChanged() {
        this._updateMonitors();
        this._updateBoxes();
        this._updateHotCorners();
        this._updateBackgrounds();
        this._updateFullscreen();
        this._updateVisibility();
        this._queueUpdateRegions();

        this.emit('monitors-changed');
    }

    _isAboveOrBelowPrimary(monitor) {
        let primary = this.monitors[this.primaryIndex];
        let monitorLeft = monitor.x, monitorRight = monitor.x + monitor.width;
        let primaryLeft = primary.x, primaryRight = primary.x + primary.width;

        if ((monitorLeft >= primaryLeft && monitorLeft < primaryRight) ||
            (monitorRight > primaryLeft && monitorRight <= primaryRight) ||
            (primaryLeft >= monitorLeft && primaryLeft < monitorRight) ||
            (primaryRight > monitorLeft && primaryRight <= monitorRight))
            return true;

        return false;
    }

    get currentMonitor() {
        let index = global.display.get_current_monitor();
        return this.monitors[index];
    }

    get keyboardMonitor() {
        return this.monitors[this.keyboardIndex];
    }

    get focusIndex() {
        let i = Main.layoutManager.primaryIndex;

        if (global.stage.key_focus != null)
            i = this.findIndexForActor(global.stage.key_focus);
        else if (global.display.focus_window != null)
            i = global.display.focus_window.get_monitor();
        return i;
    }

    get focusMonitor() {
        if (this.focusIndex < 0)
            return null;
        return this.monitors[this.focusIndex];
    }

    set keyboardIndex(v) {
        this._keyboardIndex = v;
        this._updateKeyboardBox();
    }

    get keyboardIndex() {
        return this._keyboardIndex;
    }

    _loadBackground() {
        if (!this.primaryMonitor) {
            this._pendingLoadBackground = true;
            return;
        }
        this._systemBackground = new Background.SystemBackground();
        this._systemBackground.hide();

        global.stage.insert_child_below(this._systemBackground, null);

        const constraint = new Clutter.BindConstraint({
            source: global.stage,
            coordinate: Clutter.BindCoordinate.ALL,
        });
        this._systemBackground.add_constraint(constraint);

        let signalId = this._systemBackground.connect('loaded', () => {
            this._systemBackground.disconnect(signalId);

            // We're mostly prepared for the startup animation
            // now, but since a lot is going on asynchronously
            // during startup, let's defer the startup animation
            // until the event loop is uncontended and idle.
            // This helps to prevent us from running the animation
            // when the system is bogged down
            const id = GLib.idle_add(GLib.PRIORITY_LOW, () => {
                if (this.primaryMonitor) {
                    this._systemBackground.show();
                    global.stage.show();
                    this._prepareStartupAnimation().catch(logError);
                    return GLib.SOURCE_REMOVE;
                } else {
                    return GLib.SOURCE_CONTINUE;
                }
            });
            GLib.Source.set_name_by_id(id, '[gnome-shell] Startup Animation');
        });
    }

    // Startup Animations
    //
    // We have two different animations, depending on whether we're a greeter
    // or a normal session.
    //
    // In the greeter, we want to animate the panel from the top, and smoothly
    // fade the login dialog on top of whatever plymouth left on screen which
    // we get as a still frame background before drawing anything else.
    //
    // Here we just have the code to animate the panel, and fade up the background.
    // The login dialog animation is handled by modalDialog.js
    //
    // When starting a normal user session, we want to grow it out of the middle
    // of the screen.

    async _prepareStartupAnimation() {
        // During the initial transition, add a simple actor to block all events,
        // so they don't get delivered to X11 windows that have been transformed.
        this._coverPane = new Clutter.Actor({
            opacity: 0,
            width: global.screen_width,
            height: global.screen_height,
            reactive: true,
        });
        this.addChrome(this._coverPane);

        // Force an update of the regions before we scale the UI group to
        // get the correct allocation for the struts.
        // Do this even when we don't animate on restart, so that maximized
        // windows restore to the right size.
        this._updateRegions();

        if (Meta.is_restart()) {
            // On restart, we don't do an animation.
        } else if (Main.sessionMode.isGreeter) {
            this.panelBox.translation_y = -this.panelBox.height;
        } else {
            this.keyboardBox.hide();

            let monitor = this.primaryMonitor;

            if (!Main.sessionMode.hasOverview) {
                const x = monitor.x + monitor.width / 2.0;
                const y = monitor.y + monitor.height / 2.0;

                this.uiGroup.set_pivot_point(
                    x / global.screen_width,
                    y / global.screen_height);
                this.uiGroup.scale_x = this.uiGroup.scale_y = 0.75;
                this.uiGroup.opacity = 0;
            }

            global.window_group.set_clip(monitor.x, monitor.y, monitor.width, monitor.height);

            await this._updateBackgrounds();
        }

        // Hack: Work around grab issue when testing greeter UI in nested
        if (GLib.getenv('GDM_GREETER_TEST') === '1')
            setTimeout(() => this.emit('startup-prepared'), 200);
        else
            this.emit('startup-prepared');

        this._startupAnimation();
    }

    _startupAnimation() {
        if (Meta.is_restart())
            this._startupAnimationComplete();
        else if (Main.sessionMode.isGreeter)
            this._startupAnimationGreeter();
        else
            this._startupAnimationSession();
    }

    _startupAnimationGreeter() {
        this.panelBox.ease({
            translation_y: 0,
            duration: STARTUP_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onStopped: () => this._startupAnimationComplete(),
        });
    }

    _startupAnimationSession() {
        const onStopped = () => this._startupAnimationComplete();
        if (Main.sessionMode.hasOverview) {
            Main.overview.runStartupAnimation(onStopped);
        } else {
            this.uiGroup.ease({
                scale_x: 1,
                scale_y: 1,
                opacity: 255,
                duration: STARTUP_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onStopped,
            });
        }
    }

    _startupAnimationComplete() {
        this._coverPane.destroy();
        this._coverPane = null;

        this._systemBackground.destroy();
        this._systemBackground = null;

        this._startingUp = false;

        this.keyboardBox.show();

        if (!Main.sessionMode.isGreeter) {
            this._showSecondaryBackgrounds();
            global.window_group.remove_clip();
        }

        this._queueUpdateRegions();

        this.emit('startup-complete');
    }

    // setDummyCursorGeometry:
    //
    // The cursor dummy is a standard widget commonly used for popup
    // menus and box pointers to track, as the box pointer API only
    // tracks actors. If you want to pop up a menu based on where the
    // user clicked, or where the text cursor is, the cursor dummy
    // is what you should use. Given that the menu should not track
    // the actual mouse pointer as it moves, you need to call this
    // function before you show the menu to ensure it is at the right
    // position and has the right size.
    setDummyCursorGeometry(x, y, w, h) {
        this.dummyCursor.set_position(Math.round(x), Math.round(y));
        this.dummyCursor.set_size(Math.round(w), Math.round(h));
    }

    // addChrome:
    // @actor: an actor to add to the chrome
    // @params: (optional) additional params
    //
    // Adds @actor to the chrome, and (unless %affectsInputRegion in
    // @params is %false) extends the input region to include it.
    // Changes in @actor's size, position, and visibility will
    // automatically result in appropriate changes to the input
    // region.
    //
    // If %affectsStruts in @params is %true (and @actor is along a
    // screen edge), then @actor's size and position will also affect
    // the window manager struts. Changes to @actor's visibility will
    // NOT affect whether or not the strut is present, however.
    //
    // If %trackFullscreen in @params is %true, the actor's visibility
    // will be bound to the presence of fullscreen windows on the same
    // monitor (it will be hidden whenever a fullscreen window is visible,
    // and shown otherwise)
    addChrome(actor, params) {
        this.uiGroup.add_child(actor);
        if (this.uiGroup.contains(global.top_window_group))
            this.uiGroup.set_child_below_sibling(actor, global.top_window_group);
        this._trackActor(actor, params);
    }

    // addTopChrome:
    // @actor: an actor to add to the chrome
    // @params: (optional) additional params
    //
    // Like addChrome(), but adds @actor above all windows, including popups.
    addTopChrome(actor, params) {
        this.uiGroup.add_child(actor);
        this._trackActor(actor, params);
    }

    // trackChrome:
    // @actor: a descendant of the chrome to begin tracking
    // @params: parameters describing how to track @actor
    //
    // Tells the chrome to track @actor. This can be used to extend the
    // struts or input region to cover specific children.
    //
    // @params can have any of the same values as in addChrome(),
    // though some possibilities don't make sense. By default, @actor has
    // the same params as its chrome ancestor.
    trackChrome(actor, params = {}) {
        let ancestor = actor.get_parent();
        let index = this._findActor(ancestor);
        while (ancestor && index === -1) {
            ancestor = ancestor.get_parent();
            index = this._findActor(ancestor);
        }

        let ancestorData = ancestor
            ? this._trackedActors[index]
            : defaultParams;
        // We can't use Params.parse here because we want to drop
        // the extra values like ancestorData.actor
        for (let prop in defaultParams) {
            if (!Object.prototype.hasOwnProperty.call(params, prop))
                params[prop] = ancestorData[prop];
        }

        this._trackActor(actor, params);
    }

    // untrackChrome:
    // @actor: an actor previously tracked via trackChrome()
    //
    // Undoes the effect of trackChrome()
    untrackChrome(actor) {
        this._untrackActor(actor);
    }

    // removeChrome:
    // @actor: a chrome actor
    //
    // Removes @actor from the chrome
    removeChrome(actor) {
        this.uiGroup.remove_child(actor);
        this._untrackActor(actor);
    }

    _findActor(actor) {
        for (let i = 0; i < this._trackedActors.length; i++) {
            let actorData = this._trackedActors[i];
            if (actorData.actor === actor)
                return i;
        }
        return -1;
    }

    _trackActor(actor, params) {
        if (this._findActor(actor) !== -1)
            throw new Error('trying to re-track existing chrome actor');

        let actorData = Params.parse(params, defaultParams);
        actorData.actor = actor;
        actor.connectObject(
            'notify::visible', this._queueUpdateRegions.bind(this),
            'notify::allocation', this._queueUpdateRegions.bind(this),
            'destroy', this._untrackActor.bind(this), this);
        // Note that destroying actor will unset its parent, so we don't
        // need to connect to 'destroy' too.

        this._trackedActors.push(actorData);
        this._updateActorVisibility(actorData);
        this._queueUpdateRegions();
    }

    _untrackActor(actor) {
        let i = this._findActor(actor);

        if (i === -1)
            return;

        this._trackedActors.splice(i, 1);
        actor.disconnectObject(this);

        this._queueUpdateRegions();
    }

    _updateActorVisibility(actorData) {
        if (!actorData.trackFullscreen)
            return;

        let monitor = this.findMonitorForActor(actorData.actor);
        actorData.actor.visible = !(global.window_group.visible &&
                                    monitor &&
                                    monitor.inFullscreen);
    }

    _updateVisibility() {
        let windowsVisible = Main.sessionMode.hasWindows && !this._inOverview;

        global.window_group.visible = windowsVisible;
        global.top_window_group.visible = windowsVisible;

        this._trackedActors.forEach(this._updateActorVisibility.bind(this));
    }

    getWorkAreaForMonitor(monitorIndex) {
        // Assume that all workspaces will have the same
        // struts and pick the first one.
        let workspaceManager = global.workspace_manager;
        let ws = workspaceManager.get_workspace_by_index(0);
        return ws.get_work_area_for_monitor(monitorIndex);
    }

    _findIndexForRect(x, y, width, height) {
        const rect = new Mtk.Rectangle({
            x: Math.floor(x),
            y: Math.floor(y),
            width: Math.ceil(x + width) - Math.floor(x),
            height: Math.ceil(y + height) - Math.floor(y),
        });
        return global.display.get_monitor_index_for_rect(rect);
    }

    // This call guarantees that we return some monitor to simplify usage of it
    // In practice all tracked actors should be visible on some monitor anyway
    findIndexForActor(actor) {
        let [x, y] = actor.get_transformed_position();
        let [w, h] = actor.get_transformed_size();
        return this._findIndexForRect(x, y, w, h);
    }

    _findMonitorForIndex(index) {
        if (index >= 0 && index < this.monitors.length)
            return this.monitors[index];
        return null;
    }

    findMonitorForActor(actor) {
        return this._findMonitorForIndex(this.findIndexForActor(actor));
    }

    findMonitorForPoint(x, y) {
        return this._findMonitorForIndex(this._findIndexForRect(x, y, 1, 1));
    }

    _queueUpdateRegions() {
        if (!this._updateRegionIdle) {
            const laters = global.compositor.get_laters();
            this._updateRegionIdle = laters.add(
                Meta.LaterType.BEFORE_REDRAW, this._updateRegions.bind(this));
        }
    }

    _updateFullscreen() {
        this._updateVisibility();
        this._queueUpdateRegions();
    }

    _windowsRestacked() {
        let changed = false;

        if (this._isPopupWindowVisible !== global.top_window_group.get_children().some(isPopupMetaWindow))
            changed = true;

        if (changed) {
            this._updateVisibility();
            this._queueUpdateRegions();
        }
    }

    _updateRegions() {
        if (this._updateRegionIdle) {
            const laters = global.compositor.get_laters();
            laters.remove(this._updateRegionIdle);
            delete this._updateRegionIdle;
        }

        let rects = [], struts = [], i;
        let isPopupMenuVisible = global.top_window_group.get_children().some(isPopupMetaWindow);
        const wantsInputRegion =
            !this._startingUp &&
            !isPopupMenuVisible &&
            Main.modalCount === 0 &&
            !Meta.is_wayland_compositor();

        for (i = 0; i < this._trackedActors.length; i++) {
            let actorData = this._trackedActors[i];
            if (!(actorData.affectsInputRegion && wantsInputRegion) && !actorData.affectsStruts)
                continue;

            actorData.actor.get_allocation_box();
            let [x, y] = actorData.actor.get_transformed_position();
            let [w, h] = actorData.actor.get_transformed_size();
            x = Math.round(x);
            y = Math.round(y);
            w = Math.round(w);
            h = Math.round(h);

            if (actorData.affectsInputRegion && wantsInputRegion && actorData.actor.get_paint_visibility())
                rects.push(new Mtk.Rectangle({x, y, width: w, height: h}));

            let monitor = null;
            if (actorData.affectsStruts)
                monitor = this.findMonitorForActor(actorData.actor);

            if (monitor) {
                // Limit struts to the size of the screen
                let x1 = Math.max(x, 0);
                let x2 = Math.min(x + w, global.screen_width);
                let y1 = Math.max(y, 0);
                let y2 = Math.min(y + h, global.screen_height);

                // Metacity wants to know what side of the monitor the
                // strut is considered to be attached to. First, we find
                // the monitor that contains the strut. If the actor is
                // only touching one edge, or is touching the entire
                // border of that monitor, then it's obvious which side
                // to call it. If it's in a corner, we pick a side
                // arbitrarily. If it doesn't touch any edges, or it
                // spans the width/height across the middle of the
                // screen, then we don't create a strut for it at all.

                let side;
                if (x1 <= monitor.x && x2 >= monitor.x + monitor.width) {
                    if (y1 <= monitor.y)
                        side = Meta.Side.TOP;
                    else if (y2 >= monitor.y + monitor.height)
                        side = Meta.Side.BOTTOM;
                    else
                        continue;
                } else if (y1 <= monitor.y && y2 >= monitor.y + monitor.height) {
                    if (x1 <= monitor.x)
                        side = Meta.Side.LEFT;
                    else if (x2 >= monitor.x + monitor.width)
                        side = Meta.Side.RIGHT;
                    else
                        continue;
                } else if (x1 <= monitor.x) {
                    side = Meta.Side.LEFT;
                } else if (y1 <= monitor.y) {
                    side = Meta.Side.TOP;
                } else if (x2 >= monitor.x + monitor.width) {
                    side = Meta.Side.RIGHT;
                } else if (y2 >= monitor.y + monitor.height) {
                    side = Meta.Side.BOTTOM;
                } else {
                    continue;
                }

                const strutRect = new Mtk.Rectangle({x: x1, y: y1, width: x2 - x1, height: y2 - y1});
                let strut = new Meta.Strut({rect: strutRect, side});
                struts.push(strut);
            }
        }

        if (wantsInputRegion)
            global.set_stage_input_region(rects);

        this._isPopupWindowVisible = isPopupMenuVisible;

        let workspaceManager = global.workspace_manager;
        for (let w = 0; w < workspaceManager.n_workspaces; w++) {
            let workspace = workspaceManager.get_workspace_by_index(w);
            workspace.set_builtin_struts(struts);
        }

        return GLib.SOURCE_REMOVE;
    }

    modalEnded() {
        // We don't update the stage input region while in a modal,
        // so queue an update now.
        this._queueUpdateRegions();
    }
});


// HotCorner:
//
// This class manages a "hot corner" that can toggle switching to
// overview.
export const HotCorner = GObject.registerClass(
class HotCorner extends Clutter.Actor {
    _init(layoutManager, monitor, x, y) {
        super._init();

        // We use this flag to mark the case where the user has entered the
        // hot corner and has not left both the hot corner and a surrounding
        // guard area (the "environs"). This avoids triggering the hot corner
        // multiple times due to an accidental jitter.
        this._entered = false;

        this._monitor = monitor;

        this._x = x;
        this._y = y;

        this._setupFallbackCornerIfNeeded(layoutManager);

        this._pressureBarrier = new PressureBarrier(
            HOT_CORNER_PRESSURE_THRESHOLD,
            HOT_CORNER_PRESSURE_TIMEOUT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW);
        this._pressureBarrier.connect('trigger', this._toggleOverview.bind(this));

        let px = 0.0;
        let py = 0.0;
        if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {
            px = 1.0;
            py = 0.0;
        }

        this._ripples = new Ripples.Ripples(px, py, 'ripple-box');
        this._ripples.addTo(layoutManager.uiGroup);

        this.connect('destroy', this._onDestroy.bind(this));
    }

    setBarrierSize(size) {
        if (this._verticalBarrier) {
            this._pressureBarrier.removeBarrier(this._verticalBarrier);
            this._verticalBarrier.destroy();
            this._verticalBarrier = null;
        }

        if (this._horizontalBarrier) {
            this._pressureBarrier.removeBarrier(this._horizontalBarrier);
            this._horizontalBarrier.destroy();
            this._horizontalBarrier = null;
        }

        if (size > 0) {
            if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {
                this._verticalBarrier = new Meta.Barrier({
                    backend: global.backend,
                    x1: this._x, x2: this._x, y1: this._y, y2: this._y + size,
                    directions: Meta.BarrierDirection.NEGATIVE_X,
                });
                this._horizontalBarrier = new Meta.Barrier({
                    backend: global.backend,
                    x1: this._x - size, x2: this._x, y1: this._y, y2: this._y,
                    directions: Meta.BarrierDirection.POSITIVE_Y,
                });
            } else {
                this._verticalBarrier = new Meta.Barrier({
                    backend: global.backend,
                    x1: this._x, x2: this._x, y1: this._y, y2: this._y + size,
                    directions: Meta.BarrierDirection.POSITIVE_X,
                });
                this._horizontalBarrier = new Meta.Barrier({
                    backend: global.backend,
                    x1: this._x, x2: this._x + size, y1: this._y, y2: this._y,
                    directions: Meta.BarrierDirection.POSITIVE_Y,
                });
            }

            this._pressureBarrier.addBarrier(this._verticalBarrier);
            this._pressureBarrier.addBarrier(this._horizontalBarrier);
        }
    }

    _setupFallbackCornerIfNeeded(layoutManager) {
        const {capabilities} = global.backend;
        if ((capabilities & Meta.BackendCapabilities.BARRIERS) === 0) {
            this.set({
                name: 'hot-corner-environs',
                x: this._x,
                y: this._y,
                width: 3,
                height: 3,
                reactive: true,
            });

            this._corner = new Clutter.Actor({
                name: 'hot-corner',
                width: 1,
                height: 1,
                opacity: 0,
                reactive: true,
            });
            this._corner._delegate = this;

            this.add_child(this._corner);
            layoutManager.addChrome(this);

            if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {
                this._corner.set_position(this.width - this._corner.width, 0);
                this.set_pivot_point(1.0, 0.0);
                this.translation_x = -this.width;
            } else {
                this._corner.set_position(0, 0);
            }

            this._corner.connect('enter-event',
                this._onCornerEntered.bind(this));
            this._corner.connect('leave-event',
                this._onCornerLeft.bind(this));
        }
    }

    _onDestroy() {
        this.setBarrierSize(0);
        this._pressureBarrier.destroy();
        this._pressureBarrier = null;

        this._ripples.destroy();
    }

    _toggleOverview() {
        if (this._monitor.inFullscreen && !Main.overview.visible)
            return;

        if (Main.overview.shouldToggleByCornerOrButton()) {
            Main.overview.toggle();
            if (Main.overview.animationInProgress)
                this._ripples.playAnimation(this._x, this._y);
        }
    }

    handleDragOver(source, _actor, _x, _y, _time) {
        if (source !== Main.xdndHandler)
            return DND.DragMotionResult.CONTINUE;

        this._toggleOverview();

        return DND.DragMotionResult.CONTINUE;
    }

    _onCornerEntered() {
        if (!this._entered) {
            this._entered = true;
            this._toggleOverview();
        }
        return Clutter.EVENT_PROPAGATE;
    }

    _onCornerLeft(actor, event) {
        if (event.get_related() !== this)
            this._entered = false;
        // Consume event, otherwise this will confuse onEnvironsLeft
        return Clutter.EVENT_STOP;
    }

    vfunc_leave_event(event) {
        if (event.get_related() !== this._corner)
            this._entered = false;
        return Clutter.EVENT_PROPAGATE;
    }
});

export class PressureBarrier extends Signals.EventEmitter {
    constructor(threshold, timeout, actionMode) {
        super();

        this._threshold = threshold;
        this._timeout = timeout;
        this._actionMode = actionMode;
        this._barriers = [];
        this._eventFilter = null;

        this._isTriggered = false;
        this._reset();
    }

    addBarrier(barrier) {
        barrier._pressureHitId = barrier.connect('hit', this._onBarrierHit.bind(this));
        barrier._pressureLeftId = barrier.connect('left', this._onBarrierLeft.bind(this));

        this._barriers.push(barrier);
    }

    _disconnectBarrier(barrier) {
        barrier.disconnect(barrier._pressureHitId);
        barrier.disconnect(barrier._pressureLeftId);
    }

    removeBarrier(barrier) {
        this._disconnectBarrier(barrier);
        this._barriers.splice(this._barriers.indexOf(barrier), 1);
    }

    destroy() {
        this._barriers.forEach(this._disconnectBarrier.bind(this));
        this._barriers = [];
    }

    setEventFilter(filter) {
        this._eventFilter = filter;
    }

    _reset() {
        this._barrierEvents = [];
        this._currentPressure = 0;
        this._lastTime = 0;
    }

    _isHorizontal(barrier) {
        return barrier.y1 === barrier.y2;
    }

    _getDistanceAcrossBarrier(barrier, event) {
        if (this._isHorizontal(barrier))
            return Math.abs(event.dy);
        else
            return Math.abs(event.dx);
    }

    _getDistanceAlongBarrier(barrier, event) {
        if (this._isHorizontal(barrier))
            return Math.abs(event.dx);
        else
            return Math.abs(event.dy);
    }

    _trimBarrierEvents() {
        // Events are guaranteed to be sorted in time order from
        // oldest to newest, so just look for the first old event,
        // and then chop events after that off.
        let i = 0;
        let threshold = this._lastTime - this._timeout;

        while (i < this._barrierEvents.length) {
            let [time, distance_] = this._barrierEvents[i];
            if (time >= threshold)
                break;
            i++;
        }

        let firstNewEvent = i;

        for (i = 0; i < firstNewEvent; i++) {
            let [time_, distance] = this._barrierEvents[i];
            this._currentPressure -= distance;
        }

        this._barrierEvents = this._barrierEvents.slice(firstNewEvent);
    }

    _onBarrierLeft(barrier, _event) {
        barrier._isHit = false;
        if (this._barriers.every(b => !b._isHit)) {
            this._reset();
            this._isTriggered = false;
        }
    }

    _trigger() {
        this._isTriggered = true;
        this.emit('trigger');
        this._reset();
    }

    _onBarrierHit(barrier, event) {
        barrier._isHit = true;

        // If we've triggered the barrier, wait until the pointer has the
        // left the barrier hitbox until we trigger it again.
        if (this._isTriggered)
            return;

        if (this._eventFilter && this._eventFilter(event))
            return;

        // Throw out all events not in the proper keybinding mode
        if (!(this._actionMode & Main.actionMode))
            return;

        let slide = this._getDistanceAlongBarrier(barrier, event);
        let distance = this._getDistanceAcrossBarrier(barrier, event);

        if (distance >= this._threshold) {
            this._trigger();
            return;
        }

        // Throw out events where the cursor is move more
        // along the axis of the barrier than moving with
        // the barrier.
        if (slide > distance)
            return;

        this._lastTime = event.time;

        this._trimBarrierEvents();
        distance = Math.min(15, distance);

        this._barrierEvents.push([event.time, distance]);
        this._currentPressure += distance;

        if (this._currentPressure >= this._threshold)
            this._trigger();
    }
}

const ScreenTransition = GObject.registerClass(
class ScreenTransition extends Clutter.Actor {
    _init() {
        super._init({visible: false});
    }

    vfunc_hide() {
        this.content = null;
        super.vfunc_hide();
    }

    run() {
        if (this.visible)
            return;

        Main.uiGroup.set_child_above_sibling(this, null);

        const rect = new Mtk.Rectangle({
            x: 0,
            y: 0,
            width: global.screen_width,
            height: global.screen_height,
        });
        const [, , , scale] = global.stage.get_capture_final_size(rect);
        this.content = global.stage.paint_to_content(rect, scale, Clutter.PaintFlag.NO_CURSORS);

        this.opacity = 255;
        this.show();

        this.ease({
            opacity: 0,
            duration: SCREEN_TRANSITION_DURATION,
            delay: SCREEN_TRANSITION_DELAY,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onStopped: () => this.hide(),
        });
    }
});
(uuay)smartcardManager.js�
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import * as Signals from './signals.js';

import * as ObjectManager from './objectManager.js';

const SmartcardTokenIface = `
<node>
<interface name="org.gnome.SettingsDaemon.Smartcard.Token">
  <property name="Name" type="s" access="read"/>
  <property name="Driver" type="o" access="read"/>
  <property name="IsInserted" type="b" access="read"/>
  <property name="UsedToLogin" type="b" access="read"/>
</interface>
</node>`;

let _smartcardManager = null;

/**
 * @returns {SmartcardManager}
 */
export function getSmartcardManager() {
    if (_smartcardManager == null)
        _smartcardManager = new SmartcardManager();

    return _smartcardManager;
}

class SmartcardManager extends Signals.EventEmitter {
    constructor() {
        super();

        this._objectManager = new ObjectManager.ObjectManager({
            connection: Gio.DBus.session,
            name: 'org.gnome.SettingsDaemon.Smartcard',
            objectPath: '/org/gnome/SettingsDaemon/Smartcard',
            knownInterfaces: [SmartcardTokenIface],
            onLoaded: this._onLoaded.bind(this),
        });
        this._insertedTokens = {};
        this._loginToken = null;
    }

    _onLoaded() {
        let tokens = this._objectManager.getProxiesForInterface('org.gnome.SettingsDaemon.Smartcard.Token');

        for (let i = 0; i < tokens.length; i++)
            this._addToken(tokens[i]);

        this._objectManager.connect('interface-added', (objectManager, interfaceName, proxy) => {
            if (interfaceName === 'org.gnome.SettingsDaemon.Smartcard.Token')
                this._addToken(proxy);
        });

        this._objectManager.connect('interface-removed', (objectManager, interfaceName, proxy) => {
            if (interfaceName === 'org.gnome.SettingsDaemon.Smartcard.Token')
                this._removeToken(proxy);
        });
    }

    _updateToken(token) {
        let objectPath = token.get_object_path();

        delete this._insertedTokens[objectPath];

        if (token.IsInserted)
            this._insertedTokens[objectPath] = token;

        if (token.UsedToLogin)
            this._loginToken = token;
    }

    _addToken(token) {
        this._updateToken(token);

        token.connect('g-properties-changed', (proxy, properties) => {
            const isInsertedChanged = !!properties.lookup_value('IsInserted', null);
            if (isInsertedChanged) {
                this._updateToken(token);

                if (token.IsInserted)
                    this.emit('smartcard-inserted', token);
                else
                    this.emit('smartcard-removed', token);
            }
        });

        // Emit a smartcard-inserted at startup if it's already plugged in
        if (token.IsInserted)
            this.emit('smartcard-inserted', token);
    }

    _removeToken(token) {
        let objectPath = token.get_object_path();

        if (this._insertedTokens[objectPath] === token) {
            delete this._insertedTokens[objectPath];
            this.emit('smartcard-removed', token);
        }

        if (this._loginToken === token)
            this._loginToken = null;

        token.disconnectAll();
    }

    hasInsertedTokens() {
        return Object.keys(this._insertedTokens).length > 0;
    }

    hasInsertedLoginToken() {
        if (!this._loginToken)
            return false;

        if (!this._loginToken.IsInserted)
            return false;

        return true;
    }
}
(uuay)autoRotate.js_import Gio from 'gi://Gio';
import GObject from 'gi://GObject';

import * as SystemActions from '../../misc/systemActions.js';

import {QuickToggle, SystemIndicator} from '../quickSettings.js';

const RotationToggle = GObject.registerClass(
class RotationToggle extends QuickToggle {
    _init() {
        this._systemActions = new SystemActions.getDefault();

        super._init({
            title: _('Auto Rotate'),
        });

        this._systemActions.bind_property('can-lock-orientation',
            this, 'visible',
            GObject.BindingFlags.DEFAULT |
            GObject.BindingFlags.SYNC_CREATE);
        this._systemActions.bind_property('orientation-lock-icon',
            this, 'icon-name',
            GObject.BindingFlags.DEFAULT |
            GObject.BindingFlags.SYNC_CREATE);

        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.settings-daemon.peripherals.touchscreen',
        });
        this._settings.bind('orientation-lock',
            this, 'checked',
            Gio.SettingsBindFlags.INVERT_BOOLEAN);

        this.connect('clicked',
            () => this._systemActions.activateLockOrientation());
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this.quickSettingsItems.push(new RotationToggle());
    }
});
(uuay)errorUtils.js�// Common code for displaying errors to the user in various dialogs

function formatSyntaxErrorLocation(error) {
    const {fileName = '<unknown>', lineNumber = 0, columnNumber = 0} = error;
    return ` @ ${fileName}:${lineNumber}:${columnNumber}`;
}

function formatExceptionStack(error) {
    const {stack} = error;
    if (!stack)
        return '\n\n(No stack trace)';

    const indentedStack = stack.split('\n').map(line => `  ${line}`).join('\n');
    return `\n\nStack trace:\n${indentedStack}`;
}

function formatExceptionWithCause(error, seenCauses, showStack) {
    let fmt = showStack ? formatExceptionStack(error) : '';

    const {cause} = error;
    if (!cause)
        return fmt;

    fmt += `\nCaused by: ${cause}`;

    if (cause !== null && typeof cause === 'object') {
        if (seenCauses.has(cause))
            return fmt;  // avoid recursion
        seenCauses.add(cause);

        fmt += formatExceptionWithCause(cause, seenCauses);
    }

    return fmt;
}

/**
 * Formats a thrown exception into a string, including the stack, taking the
 * location where a SyntaxError was thrown into account.
 *
 * @param {Error} error The error to format
 * @param {object} options Formatting options
 * @param {boolean} options.showStack Whether to show the stack trace (default
 *   true)
 * @returns {string} The formatted string
 */
export function formatError(error, {showStack = true} = {}) {
    try {
        let fmt = `${error}`;
        if (error === null || typeof error !== 'object')
            return fmt;

        if (error instanceof SyntaxError) {
            fmt += formatSyntaxErrorLocation(error);
            if (showStack)
                fmt += formatExceptionStack(error);
            return fmt;
        }

        const seenCauses = new Set([error]);
        fmt += formatExceptionWithCause(error, seenCauses, showStack);
        return fmt;
    } catch (e) {
        return `(could not display error: ${e})`;
    }
}
(uuay)config.jse// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const pkg = imports.package;

/* The name of this package (not localized) */
export const PACKAGE_NAME = 'gnome-shell';
/* The version of this package */
export const PACKAGE_VERSION = '46.0';
/* 1 if networkmanager is available, 0 otherwise */
export const HAVE_NETWORKMANAGER = 1;
/* 1 if portal helper is enabled, 0 otherwise */
export const HAVE_PORTAL_HELPER = 0;
/* gettext package */
export const GETTEXT_PACKAGE = 'gnome-shell';
/* locale dir */
export const LOCALEDIR = '/usr/share/locale';
/* other standard directories */
export const LIBEXECDIR = '/usr/libexec';
export const PKGDATADIR = '/usr/share/gnome-shell';
/* g-i package versions */
export const LIBMUTTER_API_VERSION = '14';

export const HAVE_BLUETOOTH = pkg.checkSymbol('GnomeBluetooth', '3.0',
    'Client.default_adapter_state');
(uuay)powerProfiles.js/// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GObject from 'gi://GObject';


import {QuickMenuToggle, SystemIndicator} from '../quickSettings.js';

import * as PopupMenu from '../popupMenu.js';

import {loadInterfaceXML} from '../../misc/fileUtils.js';

const BUS_NAME = 'net.hadess.PowerProfiles';
const OBJECT_PATH = '/net/hadess/PowerProfiles';

const PowerProfilesIface = loadInterfaceXML('net.hadess.PowerProfiles');
const PowerProfilesProxy = Gio.DBusProxy.makeProxyWrapper(PowerProfilesIface);

const PROFILE_PARAMS = {
    'performance': {
        name: C_('Power profile', 'Performance'),
        iconName: 'power-profile-performance-symbolic',
    },

    'balanced': {
        name: C_('Power profile', 'Balanced'),
        iconName: 'power-profile-balanced-symbolic',
    },

    'power-saver': {
        name: C_('Power profile', 'Power Saver'),
        iconName: 'power-profile-power-saver-symbolic',
    },
};

const LAST_PROFILE_KEY = 'last-selected-power-profile';

const PowerProfilesToggle = GObject.registerClass(
class PowerProfilesToggle extends QuickMenuToggle {
    _init() {
        super._init({title: _('Power Mode')});

        this._profileItems = new Map();

        this.connect('clicked', () => {
            this._proxy.ActiveProfile = this.checked
                ? 'balanced'
                : global.settings.get_string(LAST_PROFILE_KEY);
        });

        this._proxy = new PowerProfilesProxy(Gio.DBus.system, BUS_NAME, OBJECT_PATH,
            (proxy, error) => {
                if (error) {
                    log(error.message);
                } else {
                    this._proxy.connect('g-properties-changed', (p, properties) => {
                        const profilesChanged = !!properties.lookup_value('Profiles', null);
                        if (profilesChanged)
                            this._syncProfiles();
                        this._sync();
                    });

                    if (this._proxy.g_name_owner)
                        this._syncProfiles();
                }
                this._sync();
            });

        this._profileSection = new PopupMenu.PopupMenuSection();
        this.menu.addMenuItem(this._profileSection);
        this.menu.setHeader('power-profile-balanced-symbolic', _('Power Mode'));
        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this.menu.addSettingsAction(_('Power Settings'),
            'gnome-power-panel.desktop');

        this._sync();
    }

    _syncProfiles() {
        this._profileSection.removeAll();
        this._profileItems.clear();

        const profiles = this._proxy.Profiles
            .map(p => p.Profile.unpack())
            .reverse();
        for (const profile of profiles) {
            const {name, iconName} = PROFILE_PARAMS[profile];
            if (!name)
                continue;

            const item = new PopupMenu.PopupImageMenuItem(name, iconName);
            item.connect('activate',
                () => (this._proxy.ActiveProfile = profile));
            this._profileItems.set(profile, item);
            this._profileSection.addMenuItem(item);
        }

        this.menuEnabled = this._profileItems.size > 2;
    }

    _sync() {
        this.visible = this._proxy.g_name_owner !== null;

        if (!this.visible)
            return;

        const {ActiveProfile: activeProfile} = this._proxy;

        for (const [profile, item] of this._profileItems) {
            item.setOrnament(profile === activeProfile
                ? PopupMenu.Ornament.CHECK
                : PopupMenu.Ornament.NONE);
        }

        const {name: subtitle, iconName} = PROFILE_PARAMS[activeProfile];
        this.set({subtitle, iconName});

        this.checked = activeProfile !== 'balanced';

        if (this.checked)
            global.settings.set_string(LAST_PROFILE_KEY, activeProfile);
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this.quickSettingsItems.push(new PowerProfilesToggle());
    }
});
(uuay)shellDBus.jsYB// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';

import * as Config from '../misc/config.js';
import * as ExtensionDownloader from './extensionDownloader.js';
import * as ExtensionUtils from '../misc/extensionUtils.js';
import * as Main from './main.js';
import * as Screenshot from './screenshot.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';
import {DBusSenderChecker} from '../misc/util.js';
import {ControlsState} from './overviewControls.js';

const GnomeShellIface = loadInterfaceXML('org.gnome.Shell');
const ScreenSaverIface = loadInterfaceXML('org.gnome.ScreenSaver');

export class GnomeShell {
    constructor() {
        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell');

        this._senderChecker = new DBusSenderChecker([
            'org.gnome.Settings',
            'org.gnome.SettingsDaemon.MediaKeys',
        ]);

        this._extensionsService = new GnomeShellExtensions();
        this._screenshotService = new Screenshot.ScreenshotService();

        this._grabbedAccelerators = new Map();
        this._grabbers = new Map();

        global.display.connect('accelerator-activated',
            (display, action, device, timestamp) => {
                this._emitAcceleratorActivated(action, device, timestamp);
            });

        this._cachedOverviewVisible = false;
        Main.overview.connect('showing',
            this._checkOverviewVisibleChanged.bind(this));
        Main.overview.connect('hidden',
            this._checkOverviewVisibleChanged.bind(this));
    }

    /**
     * This function executes arbitrary code in the main
     * loop, and returns a boolean success and
     * JSON representation of the object as a string.
     *
     * If evaluation completes without throwing an exception,
     * then the return value will be [true, JSON.stringify(result)].
     * If evaluation fails, then the return value will be
     * [false, JSON.stringify(exception)];
     *
     * @async
     * @param {...any} params - method parameters
     * @param {Gio.DBusMethodInvocation} invocation - the invocation
     * @returns {void}
     */
    async EvalAsync(params, invocation) {
        if (!global.context.unsafe_mode) {
            invocation.return_value(new GLib.Variant('(bs)', [false, '']));
            return;
        }

        const [code] = params;
        let returnValue;
        let success;
        try {
            returnValue = JSON.stringify(await eval(code));
            // A hack; DBus doesn't have null/undefined
            if (returnValue === undefined)
                returnValue = '';
            success = true;
        } catch (e) {
            returnValue = `${e}`;
            success = false;
        }
        invocation.return_value(
            new GLib.Variant('(bs)', [success, returnValue]));
    }

    /**
     * Focus the overview's search entry
     *
     * @async
     * @param {...any} params - method parameters
     * @param {Gio.DBusMethodInvocation} invocation - the invocation
     * @returns {void}
     */
    async FocusSearchAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        Main.overview.focusSearch();
        invocation.return_value(null);
    }

    /**
     * Show OSD with the specified parameters
     *
     * @async
     * @param {...any} params - method parameters
     * @param {Gio.DBusMethodInvocation} invocation - the invocation
     * @returns {void}
     */
    async ShowOSDAsync([params], invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        for (let param in params)
            params[param] = params[param].deepUnpack();

        const {
            connector,
            label,
            level,
            max_level: maxLevel,
            icon: serializedIcon,
        } = params;

        let monitorIndex = -1;
        if (connector) {
            const monitorManager = global.backend.get_monitor_manager();
            monitorIndex = monitorManager.get_monitor_for_connector(connector);
        }

        let icon = null;
        if (serializedIcon)
            icon = Gio.Icon.new_for_string(serializedIcon);

        Main.osdWindowManager.show(monitorIndex, icon, label, level, maxLevel);
        invocation.return_value(null);
    }

    /**
     * Focus specified app in the overview's app grid
     *
     * @async
     * @param {string} id - an application ID
     * @param {Gio.DBusMethodInvocation} invocation - the invocation
     * @returns {void}
     */
    async FocusAppAsync([id], invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        const appSys = Shell.AppSystem.get_default();
        if (appSys.lookup_app(id) === null) {
            invocation.return_error_literal(
                Gio.DBusError,
                Gio.DBusError.FILE_NOT_FOUND,
                `No app with ID ${id}`);
            return;
        }

        Main.overview.selectApp(id);
        invocation.return_value(null);
    }

    /**
     * Show the overview's app grid
     *
     * @async
     * @param {...any} params - method parameters
     * @param {Gio.DBusMethodInvocation} invocation - the invocation
     * @returns {void}
     */
    async ShowApplicationsAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        Main.overview.show(ControlsState.APP_GRID);
        invocation.return_value(null);
    }

    async GrabAcceleratorAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        let [accel, modeFlags, grabFlags] = params;
        let sender = invocation.get_sender();
        let bindingAction = this._grabAcceleratorForSender(accel, modeFlags, grabFlags, sender);
        invocation.return_value(GLib.Variant.new('(u)', [bindingAction]));
    }

    async GrabAcceleratorsAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        let [accels] = params;
        let sender = invocation.get_sender();
        let bindingActions = [];
        for (let i = 0; i < accels.length; i++) {
            let [accel, modeFlags, grabFlags] = accels[i];
            bindingActions.push(this._grabAcceleratorForSender(accel, modeFlags, grabFlags, sender));
        }
        invocation.return_value(GLib.Variant.new('(au)', [bindingActions]));
    }

    async UngrabAcceleratorAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        let [action] = params;
        let sender = invocation.get_sender();
        let ungrabSucceeded = this._ungrabAcceleratorForSender(action, sender);

        invocation.return_value(GLib.Variant.new('(b)', [ungrabSucceeded]));
    }

    async UngrabAcceleratorsAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        let [actions] = params;
        let sender = invocation.get_sender();
        let ungrabSucceeded = true;

        for (let i = 0; i < actions.length; i++)
            ungrabSucceeded &= this._ungrabAcceleratorForSender(actions[i], sender);

        invocation.return_value(GLib.Variant.new('(b)', [ungrabSucceeded]));
    }

    async ScreenTransitionAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        Main.layoutManager.screenTransition.run();

        invocation.return_value(null);
    }

    _emitAcceleratorActivated(action, device, timestamp) {
        let destination = this._grabbedAccelerators.get(action);
        if (!destination)
            return;

        let connection = this._dbusImpl.get_connection();
        let info = this._dbusImpl.get_info();
        let params = {
            'timestamp': GLib.Variant.new('u', timestamp),
            'action-mode': GLib.Variant.new('u', Main.actionMode),
        };

        let deviceNode = device.get_device_node();
        if (deviceNode)
            params['device-node'] = GLib.Variant.new('s', deviceNode);

        connection.emit_signal(
            destination,
            this._dbusImpl.get_object_path(),
            info?.name ?? null,
            'AcceleratorActivated',
            GLib.Variant.new('(ua{sv})', [action, params]));
    }

    _grabAcceleratorForSender(accelerator, modeFlags, grabFlags, sender) {
        let bindingAction = global.display.grab_accelerator(accelerator, grabFlags);
        if (bindingAction === Meta.KeyBindingAction.NONE)
            return Meta.KeyBindingAction.NONE;

        let bindingName = Meta.external_binding_name_for_action(bindingAction);
        Main.wm.allowKeybinding(bindingName, modeFlags);

        this._grabbedAccelerators.set(bindingAction, sender);

        if (!this._grabbers.has(sender)) {
            let id = Gio.bus_watch_name(Gio.BusType.SESSION,
                sender, 0, null, this._onGrabberBusNameVanished.bind(this));
            this._grabbers.set(sender, id);
        }

        return bindingAction;
    }

    _ungrabAccelerator(action) {
        let ungrabSucceeded = global.display.ungrab_accelerator(action);
        if (ungrabSucceeded)
            this._grabbedAccelerators.delete(action);

        return ungrabSucceeded;
    }

    _ungrabAcceleratorForSender(action, sender) {
        let grabbedBy = this._grabbedAccelerators.get(action);
        if (sender !== grabbedBy)
            return false;

        return this._ungrabAccelerator(action);
    }

    _onGrabberBusNameVanished(connection, name) {
        let grabs = this._grabbedAccelerators.entries();
        for (let [action, sender] of grabs) {
            if (sender === name)
                this._ungrabAccelerator(action);
        }
        Gio.bus_unwatch_name(this._grabbers.get(name));
        this._grabbers.delete(name);
    }

    async ShowMonitorLabelsAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        let sender = invocation.get_sender();
        let [dict] = params;
        Main.osdMonitorLabeler.show(sender, dict);
        invocation.return_value(null);
    }

    async HideMonitorLabelsAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        let sender = invocation.get_sender();
        Main.osdMonitorLabeler.hide(sender);
        invocation.return_value(null);
    }

    _checkOverviewVisibleChanged() {
        if (Main.overview.visible !== this._cachedOverviewVisible) {
            this._cachedOverviewVisible = Main.overview.visible;
            this._dbusImpl.emit_property_changed('OverviewActive', new GLib.Variant('b', this._cachedOverviewVisible));
        }
    }

    get Mode() {
        return global.session_mode;
    }

    get OverviewActive() {
        return this._cachedOverviewVisible;
    }

    set OverviewActive(visible) {
        if (visible)
            Main.overview.show();
        else
            Main.overview.hide();
    }

    get ShellVersion() {
        return Config.PACKAGE_VERSION;
    }
}

const GnomeShellExtensionsIface = loadInterfaceXML('org.gnome.Shell.Extensions');

class GnomeShellExtensions {
    constructor() {
        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellExtensionsIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell');

        this._userExtensionsEnabled = this.UserExtensionsEnabled;
        global.settings.connect('changed::disable-user-extensions', () => {
            if (this._userExtensionsEnabled === this.UserExtensionsEnabled)
                return;

            this._userExtensionsEnabled = this.UserExtensionsEnabled;
            this._dbusImpl.emit_property_changed('UserExtensionsEnabled',
                new GLib.Variant('b', this._userExtensionsEnabled));
        });

        Main.extensionManager.connect('extension-state-changed',
            this._extensionStateChanged.bind(this));
    }

    ListExtensions() {
        let out = {};
        Main.extensionManager.getUuids().forEach(uuid => {
            let dbusObj = this.GetExtensionInfo(uuid);
            out[uuid] = dbusObj;
        });
        return out;
    }

    GetExtensionInfo(uuid) {
        let extension = Main.extensionManager.lookup(uuid) || {};
        return ExtensionUtils.serializeExtension(extension);
    }

    GetExtensionErrors(uuid) {
        let extension = Main.extensionManager.lookup(uuid);
        if (!extension)
            return [];

        if (!extension.errors)
            return [];

        return extension.errors;
    }

    InstallRemoteExtensionAsync([uuid], invocation) {
        return ExtensionDownloader.installExtension(uuid, invocation);
    }

    UninstallExtension(uuid) {
        return ExtensionDownloader.uninstallExtension(uuid);
    }

    EnableExtension(uuid) {
        return Main.extensionManager.enableExtension(uuid);
    }

    DisableExtension(uuid) {
        return Main.extensionManager.disableExtension(uuid);
    }

    LaunchExtensionPrefs(uuid) {
        this.OpenExtensionPrefs(uuid, '', {});
    }

    OpenExtensionPrefs(uuid, parentWindow, options) {
        Main.extensionManager.openExtensionPrefs(uuid, parentWindow, options);
    }

    ReloadExtensionAsync(params, invocation) {
        invocation.return_error_literal(
            Gio.DBusError,
            Gio.DBusError.NOT_SUPPORTED,
            'ReloadExtension is deprecated and does not work');
    }

    CheckForUpdates() {
        ExtensionDownloader.checkForUpdates();
    }

    get ShellVersion() {
        return Config.PACKAGE_VERSION;
    }

    get UserExtensionsEnabled() {
        return !global.settings.get_boolean('disable-user-extensions');
    }

    set UserExtensionsEnabled(enable) {
        global.settings.set_boolean('disable-user-extensions', !enable);
    }

    _extensionStateChanged(_, newState) {
        let state = ExtensionUtils.serializeExtension(newState);
        this._dbusImpl.emit_signal('ExtensionStateChanged',
            new GLib.Variant('(sa{sv})', [newState.uuid, state]));

        this._dbusImpl.emit_signal('ExtensionStatusChanged',
            new GLib.Variant('(sis)', [newState.uuid, newState.state, newState.error]));
    }
}

export class ScreenSaverDBus {
    constructor(screenShield) {
        this._screenShield = screenShield;
        screenShield.connect('active-changed', shield => {
            this._dbusImpl.emit_signal('ActiveChanged', GLib.Variant.new('(b)', [shield.active]));
        });
        screenShield.connect('wake-up-screen', () => {
            this._dbusImpl.emit_signal('WakeUpScreen', null);
        });

        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(ScreenSaverIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gnome/ScreenSaver');

        Gio.DBus.session.own_name('org.gnome.Shell.ScreenShield',
            Gio.BusNameOwnerFlags.NONE, null, null);
    }

    LockAsync(parameters, invocation) {
        let tmpId = this._screenShield.connect('lock-screen-shown', () => {
            this._screenShield.disconnect(tmpId);

            invocation.return_value(null);
        });

        this._screenShield.lock(true);
    }

    SetActive(active) {
        if (active)
            this._screenShield.activate(true);
        else
            this._screenShield.deactivate(false);
    }

    GetActive() {
        return this._screenShield.active;
    }

    GetActiveTime() {
        let started = this._screenShield.activationTime;
        if (started > 0)
            return Math.floor((GLib.get_monotonic_time() - started) / 1000000);
        else
            return 0;
    }
}
(uuay)modalDialog.js!// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Dialog from './dialog.js';
import * as Layout from './layout.js';
import * as Lightbox from './lightbox.js';
import * as Main from './main.js';
import * as Params from '../misc/params.js';

const OPEN_AND_CLOSE_TIME = 100;
const FADE_OUT_DIALOG_TIME = 1000;

/** @enum {number} */
export const State = {
    OPENED: 0,
    CLOSED: 1,
    OPENING: 2,
    CLOSING: 3,
    FADED_OUT: 4,
};

export const ModalDialog = GObject.registerClass({
    Properties: {
        'state': GObject.ParamSpec.int(
            'state', 'Dialog state', 'state',
            GObject.ParamFlags.READABLE,
            Math.min(...Object.values(State)),
            Math.max(...Object.values(State)),
            State.CLOSED),
    },
    Signals: {'opened': {}, 'closed': {}},
}, class ModalDialog extends St.Widget {
    _init(params) {
        super._init({
            visible: false,
            reactive: true,
            x: 0,
            y: 0,
            accessible_role: Atk.Role.DIALOG,
        });

        params = Params.parse(params, {
            shellReactive: false,
            styleClass: null,
            actionMode: Shell.ActionMode.SYSTEM_MODAL,
            shouldFadeIn: true,
            shouldFadeOut: true,
            destroyOnClose: true,
        });

        this._state = State.CLOSED;
        this._hasModal = false;
        this._actionMode = params.actionMode;
        this._shellReactive = params.shellReactive;
        this._shouldFadeIn = params.shouldFadeIn;
        this._shouldFadeOut = params.shouldFadeOut;
        this._destroyOnClose = params.destroyOnClose;

        Main.layoutManager.modalDialogGroup.add_child(this);

        const constraint = new Clutter.BindConstraint({
            source: global.stage,
            coordinate: Clutter.BindCoordinate.ALL,
        });
        this.add_constraint(constraint);

        this.backgroundStack = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
        });
        this._backgroundBin = new St.Bin({child: this.backgroundStack});
        this._monitorConstraint = new Layout.MonitorConstraint();
        this._backgroundBin.add_constraint(this._monitorConstraint);
        this.add_child(this._backgroundBin);

        this.dialogLayout = new Dialog.Dialog(this.backgroundStack, params.styleClass);
        this.contentLayout = this.dialogLayout.contentLayout;
        this.buttonLayout = this.dialogLayout.buttonLayout;

        if (!this._shellReactive) {
            this._lightbox = new Lightbox.Lightbox(this, {
                inhibitEvents: true,
                radialEffect: true,
            });
            this._lightbox.highlight(this._backgroundBin);

            this._eventBlocker = new Clutter.Actor({reactive: true});
            this.backgroundStack.add_child(this._eventBlocker);
        }

        global.focus_manager.add_group(this.dialogLayout);
        this._initialKeyFocus = null;
        this._initialKeyFocusDestroyId = 0;
        this._savedKeyFocus = null;
    }

    get state() {
        return this._state;
    }

    _setState(state) {
        if (this._state === state)
            return;

        this._state = state;
        this.notify('state');
    }

    vfunc_key_press_event(event) {
        if (global.focus_manager.navigate_from_event(event))
            return Clutter.EVENT_STOP;

        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_captured_event(event) {
        if (Main.keyboard.maybeHandleEvent(event))
            return Clutter.EVENT_STOP;

        return Clutter.EVENT_PROPAGATE;
    }

    clearButtons() {
        this.dialogLayout.clearButtons();
    }

    setButtons(buttons) {
        this.clearButtons();

        for (let buttonInfo of buttons)
            this.addButton(buttonInfo);
    }

    addButton(buttonInfo) {
        return this.dialogLayout.addButton(buttonInfo);
    }

    _fadeOpen() {
        this._monitorConstraint.index = global.display.get_current_monitor();

        this._setState(State.OPENING);

        this.dialogLayout.opacity = 255;
        if (this._lightbox)
            this._lightbox.lightOn();
        this.opacity = 0;
        this.show();
        this.ease({
            opacity: 255,
            duration: this._shouldFadeIn ? OPEN_AND_CLOSE_TIME : 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this._setState(State.OPENED);
                this.emit('opened');
            },
        });
    }

    setInitialKeyFocus(actor) {
        this._initialKeyFocus?.disconnectObject(this);

        this._initialKeyFocus = actor;

        actor.connectObject('destroy',
            () => (this._initialKeyFocus = null), this);
    }

    open() {
        if (this.state === State.OPENED || this.state === State.OPENING)
            return true;

        if (!this.pushModal())
            return false;

        this._fadeOpen();
        return true;
    }

    _closeComplete() {
        this._setState(State.CLOSED);
        this.hide();
        this.emit('closed');

        if (this._destroyOnClose)
            this.destroy();
    }

    close(timestamp) {
        if (this.state === State.CLOSED || this.state === State.CLOSING)
            return;

        this._setState(State.CLOSING);
        this.popModal(timestamp);
        this._savedKeyFocus = null;

        if (this._shouldFadeOut) {
            this.ease({
                opacity: 0,
                duration: OPEN_AND_CLOSE_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => this._closeComplete(),
            });
        } else {
            this._closeComplete();
        }
    }

    // Drop modal status without closing the dialog; this makes the
    // dialog insensitive as well, so it needs to be followed shortly
    // by either a close() or a pushModal()
    popModal() {
        if (!this._hasModal)
            return;

        let focus = global.stage.key_focus;
        if (focus && this.contains(focus))
            this._savedKeyFocus = focus;
        else
            this._savedKeyFocus = null;
        Main.popModal(this._grab);
        this._grab = null;
        this._hasModal = false;

        if (!this._shellReactive)
            this.backgroundStack.set_child_above_sibling(this._eventBlocker, null);
    }

    pushModal() {
        if (this._hasModal)
            return true;

        const grab = Main.pushModal(this, {actionMode: this._actionMode});
        if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
            Main.popModal(grab);
            return false;
        }

        this._grab = grab;
        Main.layoutManager.emit('system-modal-opened');

        this._hasModal = true;
        if (this._savedKeyFocus) {
            this._savedKeyFocus.grab_key_focus();
            this._savedKeyFocus = null;
        } else {
            let focus = this._initialKeyFocus || this.dialogLayout.initialKeyFocus;
            focus.grab_key_focus();
        }

        if (!this._shellReactive)
            this.backgroundStack.set_child_below_sibling(this._eventBlocker, null);
        return true;
    }

    // This method is like close, but fades the dialog out much slower,
    // and leaves the lightbox in place. Once in the faded out state,
    // the dialog can be brought back by an open call, or the lightbox
    // can be dismissed by a close call.
    //
    // The main point of this method is to give some indication to the user
    // that the dialog response has been acknowledged but will take a few
    // moments before being processed.
    // e.g., if a user clicked "Log Out" then the dialog should go away
    // immediately, but the lightbox should remain until the logout is
    // complete.
    _fadeOutDialog() {
        if (this.state === State.CLOSED || this.state === State.CLOSING)
            return;

        if (this.state === State.FADED_OUT)
            return;

        this.popModal();
        this.dialogLayout.ease({
            opacity: 0,
            duration: FADE_OUT_DIALOG_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this._setState(State.FADED_OUT),
        });
    }
});
(uuay)overview.js;U// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';

// Time for initial animation going into Overview mode;
// this is defined here to make it available in imports.
export const ANIMATION_TIME = 250;

import * as DND from './dnd.js';
import * as LayoutManager from './layout.js';
import * as Main from './main.js';
import * as MessageTray from './messageTray.js';
import * as OverviewControls from './overviewControls.js';
import * as Params from '../misc/params.js';
import * as SwipeTracker from './swipeTracker.js';
import * as WindowManager from './windowManager.js';
import * as WorkspaceThumbnail from './workspaceThumbnail.js';

const DND_WINDOW_SWITCH_TIMEOUT = 750;

const OVERVIEW_ACTIVATION_TIMEOUT = 0.5;

class ShellInfo {
    setMessage(title, options) {
        options = Params.parse(options, {
            undoCallback: null,
            forFeedback: false,
        });

        const source = MessageTray.getSystemSource();
        let undoCallback = options.undoCallback;
        let forFeedback = options.forFeedback;

        if (!this._notification) {
            this._notification = new MessageTray.Notification({
                source,
                isTransient: true,
                forFeedback,
            });
            this._notification.connect('destroy', () => delete this._notification);
        }
        this._notification.set({title});

        this._notification.clearActions();

        if (undoCallback)
            this._notification.addAction(_('Undo'), () => undoCallback());

        source.addNotification(this._notification);
    }
}

const OverviewActor = GObject.registerClass(
class OverviewActor extends St.BoxLayout {
    _init() {
        super._init({
            name: 'overview',
            /* Translators: This is the main view to select
                activities. See also note for "Activities" string. */
            accessible_name: _('Overview'),
            vertical: true,
        });

        this.add_constraint(new LayoutManager.MonitorConstraint({primary: true}));

        this._controls = new OverviewControls.ControlsManager();
        this.add_child(this._controls);
    }

    prepareToEnterOverview() {
        this._controls.prepareToEnterOverview();
    }

    prepareToLeaveOverview() {
        this._controls.prepareToLeaveOverview();
    }

    animateToOverview(state, callback) {
        this._controls.animateToOverview(state, callback);
    }

    animateFromOverview(callback) {
        this._controls.animateFromOverview(callback);
    }

    runStartupAnimation(callback) {
        this._controls.runStartupAnimation(callback);
    }

    get dash() {
        return this._controls.dash;
    }

    get searchController() {
        return this._controls.searchController;
    }

    get searchEntry() {
        return this._controls.searchEntry;
    }

    get controls() {
        return this._controls;
    }
});

const OverviewShownState = {
    HIDDEN: 'HIDDEN',
    HIDING: 'HIDING',
    SHOWING: 'SHOWING',
    SHOWN: 'SHOWN',
};

const OVERVIEW_SHOWN_TRANSITIONS = {
    [OverviewShownState.HIDDEN]: {
        signal: 'hidden',
        allowedTransitions: [OverviewShownState.SHOWING],
    },
    [OverviewShownState.HIDING]: {
        signal: 'hiding',
        allowedTransitions:
            [OverviewShownState.HIDDEN, OverviewShownState.SHOWING],
    },
    [OverviewShownState.SHOWING]: {
        signal: 'showing',
        allowedTransitions:
            [OverviewShownState.SHOWN, OverviewShownState.HIDING],
    },
    [OverviewShownState.SHOWN]: {
        signal: 'shown',
        allowedTransitions: [OverviewShownState.HIDING],
    },
};

export class Overview extends Signals.EventEmitter {
    constructor() {
        super();

        this._initCalled = false;
        this._visible = false;

        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
        this._sessionUpdated();
    }

    get dash() {
        return this._overview.dash;
    }

    get dashIconSize() {
        logError(new Error('Usage of Overview.\'dashIconSize\' is deprecated, ' +
            'use \'dash.iconSize\' property instead'));
        return this.dash.iconSize;
    }

    get animationInProgress() {
        return this._animationInProgress;
    }

    get visible() {
        return this._visible;
    }

    get visibleTarget() {
        return this._visibleTarget;
    }

    get closing() {
        return this._animationInProgress && !this._visibleTarget;
    }

    _createOverview() {
        if (this._overview)
            return;

        if (this.isDummy)
            return;

        this._activationTime = 0;

        this._visible = false;          // animating to overview, in overview, animating out
        this._shown = false;            // show() and not hide()
        this._modal = false;            // have a modal grab
        this._animationInProgress = false;
        this._visibleTarget = false;
        this._shownState = OverviewShownState.HIDDEN;

        // During transitions, we raise this to the top to avoid having the overview
        // area be reactive; it causes too many issues such as double clicks on
        // Dash elements, or mouseover handlers in the workspaces.
        this._coverPane = new Clutter.Actor({
            opacity: 0,
            reactive: true,
        });
        Main.layoutManager.overviewGroup.add_child(this._coverPane);
        this._coverPane.connect('event', (_actor, event) => {
            return event.type() === Clutter.EventType.ENTER ||
                event.type() === Clutter.EventType.LEAVE
                ? Clutter.EVENT_PROPAGATE : Clutter.EVENT_STOP;
        });
        this._coverPane.hide();

        // XDND
        this._dragMonitor = {
            dragMotion: this._onDragMotion.bind(this),
        };


        Main.layoutManager.overviewGroup.connect('scroll-event',
            this._onScrollEvent.bind(this));
        Main.xdndHandler.connect('drag-begin', this._onDragBegin.bind(this));
        Main.xdndHandler.connect('drag-end', this._onDragEnd.bind(this));

        global.display.connect('restacked', this._onRestacked.bind(this));

        this._windowSwitchTimeoutId = 0;
        this._windowSwitchTimestamp = 0;
        this._lastActiveWorkspaceIndex = -1;
        this._lastHoveredWindow = null;

        if (this._initCalled)
            this.init();
    }

    _sessionUpdated() {
        const {hasOverview} = Main.sessionMode;
        if (!hasOverview)
            this.hide();

        this.isDummy = !hasOverview;
        this._createOverview();
    }

    // The members we construct that are implemented in JS might
    // want to access the overview as Main.overview to connect
    // signal handlers and so forth. So we create them after
    // construction in this init() method.
    init() {
        this._initCalled = true;

        if (this.isDummy)
            return;

        this._overview = new OverviewActor();
        this._overview._delegate = this;
        Main.layoutManager.overviewGroup.add_child(this._overview);

        this._shellInfo = new ShellInfo();

        Main.layoutManager.connect('monitors-changed', this._relayout.bind(this));
        this._relayout();

        Main.wm.addKeybinding(
            'toggle-overview',
            new Gio.Settings({schema_id: WindowManager.SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this.toggle.bind(this));

        const swipeTracker = new SwipeTracker.SwipeTracker(global.stage,
            Clutter.Orientation.VERTICAL,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            {allowDrag: false, allowScroll: false});
        swipeTracker.orientation = Clutter.Orientation.VERTICAL;
        swipeTracker.connect('begin', this._gestureBegin.bind(this));
        swipeTracker.connect('update', this._gestureUpdate.bind(this));
        swipeTracker.connect('end', this._gestureEnd.bind(this));
        this._swipeTracker = swipeTracker;
    }

    //
    // options:
    //  - undoCallback (function): the callback to be called if undo support is needed
    //  - forFeedback (boolean): whether the message is for direct feedback of a user action
    //
    setMessage(text, options) {
        if (this.isDummy)
            return;

        this._shellInfo.setMessage(text, options);
    }

    _changeShownState(state) {
        const {allowedTransitions} =
            OVERVIEW_SHOWN_TRANSITIONS[this._shownState];

        if (!allowedTransitions.includes(state)) {
            throw new Error('Invalid overview shown transition from ' +
                `${this._shownState} to ${state}`);
        }

        if (this._shownState === OverviewShownState.HIDDEN)
            Meta.disable_unredirect_for_display(global.display);
        else if (state === OverviewShownState.HIDDEN)
            Meta.enable_unredirect_for_display(global.display);

        this._shownState = state;
        this.emit(OVERVIEW_SHOWN_TRANSITIONS[state].signal);
    }

    _onDragBegin() {
        this._inXdndDrag = true;

        DND.addDragMonitor(this._dragMonitor);
        // Remember the workspace we started from
        let workspaceManager = global.workspace_manager;
        this._lastActiveWorkspaceIndex = workspaceManager.get_active_workspace_index();
    }

    _onDragEnd() {
        this._inXdndDrag = false;

        // In case the drag was canceled while in the overview
        // we have to go back to where we started and hide
        // the overview
        if (this._shown) {
            let workspaceManager = global.workspace_manager;
            workspaceManager.get_workspace_by_index(this._lastActiveWorkspaceIndex)
                .activate(global.get_current_time());
            this.hide();
        }
        this._resetWindowSwitchTimeout();
        this._lastHoveredWindow = null;
        DND.removeDragMonitor(this._dragMonitor);
        this.endItemDrag();
    }

    _resetWindowSwitchTimeout() {
        if (this._windowSwitchTimeoutId !== 0) {
            GLib.source_remove(this._windowSwitchTimeoutId);
            this._windowSwitchTimeoutId = 0;
        }
    }

    _onDragMotion(dragEvent) {
        let targetIsWindow = dragEvent.targetActor &&
                             dragEvent.targetActor._delegate &&
                             dragEvent.targetActor._delegate.metaWindow &&
                             !(dragEvent.targetActor._delegate instanceof WorkspaceThumbnail.WindowClone);

        this._windowSwitchTimestamp = global.get_current_time();

        if (targetIsWindow &&
            dragEvent.targetActor._delegate.metaWindow === this._lastHoveredWindow)
            return DND.DragMotionResult.CONTINUE;

        this._lastHoveredWindow = null;

        this._resetWindowSwitchTimeout();

        if (targetIsWindow) {
            this._lastHoveredWindow = dragEvent.targetActor._delegate.metaWindow;
            this._windowSwitchTimeoutId = GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                DND_WINDOW_SWITCH_TIMEOUT,
                () => {
                    this._windowSwitchTimeoutId = 0;
                    Main.activateWindow(dragEvent.targetActor._delegate.metaWindow,
                        this._windowSwitchTimestamp);
                    this.hide();
                    this._lastHoveredWindow = null;
                    return GLib.SOURCE_REMOVE;
                });
            GLib.Source.set_name_by_id(this._windowSwitchTimeoutId, '[gnome-shell] Main.activateWindow');
        }

        return DND.DragMotionResult.CONTINUE;
    }

    _onScrollEvent(actor, event) {
        this.emit('scroll-event', event);
        return Clutter.EVENT_PROPAGATE;
    }

    _relayout() {
        // To avoid updating the position and size of the workspaces
        // we just hide the overview. The positions will be updated
        // when it is next shown.
        this.hide();

        this._coverPane.set_position(0, 0);
        this._coverPane.set_size(global.screen_width, global.screen_height);
    }

    _onRestacked() {
        let stack = global.get_window_actors();
        let stackIndices = {};

        for (let i = 0; i < stack.length; i++) {
            // Use the stable sequence for an integer to use as a hash key
            stackIndices[stack[i].get_meta_window().get_stable_sequence()] = i;
        }

        this.emit('windows-restacked', stackIndices);
    }

    _gestureBegin(tracker) {
        this._overview.controls.gestureBegin(tracker);
    }

    _gestureUpdate(tracker, progress) {
        if (!this._shown) {
            this._shown = true;
            this._visible = true;
            this._visibleTarget = true;
            this._animationInProgress = true;

            Main.layoutManager.overviewGroup.set_child_above_sibling(
                this._coverPane, null);
            this._coverPane.show();
            this._changeShownState(OverviewShownState.SHOWING);

            Main.layoutManager.showOverview();
            this._syncGrab();
        }

        this._overview.controls.gestureProgress(progress);
    }

    _gestureEnd(tracker, duration, endProgress) {
        let onComplete;
        if (endProgress === 0) {
            this._shown = false;
            this._visibleTarget = false;
            this._changeShownState(OverviewShownState.HIDING);
            Main.panel.style = `transition-duration: ${duration}ms;`;
            onComplete = () => this._hideDone();
        } else {
            onComplete = () => this._showDone();
        }

        this._overview.controls.gestureEnd(endProgress, duration, onComplete);
    }

    beginItemDrag(source) {
        this.emit('item-drag-begin', source);
        this._inItemDrag = true;
    }

    cancelledItemDrag(source) {
        this.emit('item-drag-cancelled', source);
    }

    endItemDrag(source) {
        if (!this._inItemDrag)
            return;
        this.emit('item-drag-end', source);
        this._inItemDrag = false;
    }

    beginWindowDrag(window) {
        this.emit('window-drag-begin', window);
        this._inWindowDrag = true;
    }

    cancelledWindowDrag(window) {
        this.emit('window-drag-cancelled', window);
    }

    endWindowDrag(window) {
        if (!this._inWindowDrag)
            return;
        this.emit('window-drag-end', window);
        this._inWindowDrag = false;
    }

    focusSearch() {
        this.show();
        this._overview.searchEntry.grab_key_focus();
    }

    // Checks if the Activities button is currently sensitive to
    // clicks. The first call to this function within the
    // OVERVIEW_ACTIVATION_TIMEOUT time of the hot corner being
    // triggered will return false. This avoids opening and closing
    // the overview if the user both triggered the hot corner and
    // clicked the Activities button.
    shouldToggleByCornerOrButton() {
        if (this._animationInProgress)
            return false;
        if (this._inItemDrag || this._inWindowDrag)
            return false;
        if (!this._activationTime ||
            GLib.get_monotonic_time() / GLib.USEC_PER_SEC - this._activationTime > OVERVIEW_ACTIVATION_TIMEOUT)
            return true;
        return false;
    }

    _syncGrab() {
        // We delay grab changes during animation so that when removing the
        // overview we don't have a problem with the release of a press/release
        // going to an application.
        if (this._animationInProgress)
            return true;

        if (this._shown) {
            let shouldBeModal = !this._inXdndDrag;
            if (shouldBeModal && !this._modal) {
                if (global.display.is_grabbed()) {
                    this.hide();
                    return false;
                }

                const grab = Main.pushModal(global.stage, {
                    actionMode: Shell.ActionMode.OVERVIEW,
                });
                if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
                    Main.popModal(grab);
                    this.hide();
                    return false;
                }

                this._grab = grab;
                this._modal = true;
            }
        } else {
            // eslint-disable-next-line no-lonely-if
            if (this._modal) {
                Main.popModal(this._grab);
                this._grab = false;
                this._modal = false;
            }
        }
        return true;
    }

    // show:
    //
    // Animates the overview visible and grabs mouse and keyboard input
    show(state = OverviewControls.ControlsState.WINDOW_PICKER) {
        if (state === OverviewControls.ControlsState.HIDDEN)
            throw new Error('Invalid state, use hide() to hide');

        if (this.isDummy)
            return;
        if (this._shown)
            return;
        this._shown = true;

        if (!this._syncGrab())
            return;

        Main.layoutManager.showOverview();
        this._animateVisible(state);
    }


    _animateVisible(state) {
        if (this._visible || this._animationInProgress)
            return;

        this._visible = true;
        this._animationInProgress = true;
        this._visibleTarget = true;
        this._activationTime = GLib.get_monotonic_time() / GLib.USEC_PER_SEC;

        Main.layoutManager.overviewGroup.set_child_above_sibling(
            this._coverPane, null);
        this._coverPane.show();

        this._overview.prepareToEnterOverview();
        this._changeShownState(OverviewShownState.SHOWING);
        this._overview.animateToOverview(state, () => this._showDone());
    }

    _showDone() {
        this._animationInProgress = false;
        this._coverPane.hide();

        if (this._shownState !== OverviewShownState.SHOWN)
            this._changeShownState(OverviewShownState.SHOWN);

        // Handle any calls to hide* while we were showing
        if (!this._shown)
            this._animateNotVisible();

        this._syncGrab();
    }

    // hide:
    //
    // Reverses the effect of show()
    hide() {
        if (this.isDummy)
            return;

        if (!this._shown)
            return;

        let event = Clutter.get_current_event();
        if (event) {
            let type = event.type();
            const button =
                type === Clutter.EventType.BUTTON_PRESS ||
                type === Clutter.EventType.BUTTON_RELEASE;
            let ctrl = (event.get_state() & Clutter.ModifierType.CONTROL_MASK) !== 0;
            if (button && ctrl)
                return;
        }

        this._shown = false;

        this._animateNotVisible();
        this._syncGrab();
    }

    _animateNotVisible() {
        if (!this._visible || this._animationInProgress)
            return;

        this._animationInProgress = true;
        this._visibleTarget = false;

        Main.layoutManager.overviewGroup.set_child_above_sibling(
            this._coverPane, null);
        this._coverPane.show();

        this._overview.prepareToLeaveOverview();
        this._changeShownState(OverviewShownState.HIDING);
        this._overview.animateFromOverview(() => this._hideDone());
    }

    _hideDone() {
        this._coverPane.hide();

        this._visible = false;
        this._animationInProgress = false;

        // Handle any calls to show* while we were hiding
        if (this._shown) {
            this._changeShownState(OverviewShownState.HIDDEN);
            this._animateVisible(OverviewControls.ControlsState.WINDOW_PICKER);
        } else {
            Main.layoutManager.hideOverview();
            this._changeShownState(OverviewShownState.HIDDEN);
        }

        Main.panel.style = null;

        this._syncGrab();
    }

    toggle() {
        if (this.isDummy)
            return;

        if (this._visible)
            this.hide();
        else
            this.show();
    }

    showApps() {
        this.show(OverviewControls.ControlsState.APP_GRID);
    }

    selectApp(id) {
        this.showApps();
        this._overview.controls.appDisplay.selectApp(id);
    }

    runStartupAnimation(callback) {
        Main.panel.style = 'transition-duration: 0ms;';

        this._shown = true;
        this._visible = true;
        this._visibleTarget = true;
        Main.layoutManager.showOverview();
        // We should call this._syncGrab() here, but moved it to happen after
        // the animation because of a race in the xserver where the grab
        // fails when requested very early during startup.

        this._changeShownState(OverviewShownState.SHOWING);

        this._overview.runStartupAnimation(() => {
            // Overview got hidden during startup animation
            if (this._shownState !== OverviewShownState.SHOWING) {
                callback();
                return;
            }

            if (!this._syncGrab()) {
                callback();
                this.hide();
                return;
            }

            Main.panel.style = null;
            this._changeShownState(OverviewShownState.SHOWN);
            callback();
        });
    }

    getShowAppsButton() {
        logError(new Error('Usage of Overview.\'getShowAppsButton\' is deprecated, ' +
            'use \'dash.showAppsButton\' property instead'));

        return this.dash.showAppsButton;
    }

    get searchController() {
        return this._overview.searchController;
    }

    get searchEntry() {
        return this._overview.searchEntry;
    }
}
(uuay)dateUtils.jsRimport * as System from 'system';
import * as Gettext from 'gettext';
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import Shell from 'gi://Shell';

import * as Params from './params.js';

let _desktopSettings = null;
let _localTimeZone = null;

/**
 * @private
 *
 * @param {Date} date a Date object
 * @returns {GLib.DateTime | null}
 */
function _convertJSDateToGLibDateTime(date) {
    if (_localTimeZone === null)
        _localTimeZone = GLib.TimeZone.new_local();

    const dt = GLib.DateTime.new(_localTimeZone,
        date.getFullYear(),
        date.getMonth() + 1,
        date.getDate(),
        date.getHours(),
        date.getMinutes(),
        date.getSeconds());

    return dt;
}

/**
 * Formats a Date object according to a C sprintf-style string using
 * the cached local timezone.
 *
 * @param {Date} date a Date object
 * @param {string} format a format String for the date
 * @returns {string}
 */
export function formatDateWithCFormatString(date, format) {
    const dt = _convertJSDateToGLibDateTime(date);

    return dt?.format(format) ?? '';
}

/**
 * Formats a time span string representing the
 * date passed in to the current time.
 *
 * @param {Date} date the start of the time span
 * @returns {string}
 */
export function formatTimeSpan(date) {
    if (_localTimeZone === null)
        _localTimeZone = GLib.TimeZone.new_local();

    const now = GLib.DateTime.new_now(_localTimeZone);
    const timespan = now.difference(date);

    const minutesAgo = timespan / GLib.TIME_SPAN_MINUTE;
    const hoursAgo = timespan / GLib.TIME_SPAN_HOUR;
    const daysAgo = timespan / GLib.TIME_SPAN_DAY;
    const weeksAgo = daysAgo / 7;
    const monthsAgo = daysAgo / 30;
    const yearsAgo = weeksAgo / 52;

    if (minutesAgo < 5)
        return _('Just now');
    if (hoursAgo < 1) {
        return Gettext.ngettext(
            '%d minute ago',
            '%d minutes ago',
            minutesAgo
        ).format(minutesAgo);
    }
    if (daysAgo < 1) {
        return Gettext.ngettext(
            '%d hour ago',
            '%d hours ago',
            hoursAgo
        ).format(hoursAgo);
    }
    if (daysAgo < 2)
        return _('Yesterday');
    if (daysAgo < 15) {
        return Gettext.ngettext(
            '%d day ago',
            '%d days ago',
            daysAgo
        ).format(daysAgo);
    }
    if (weeksAgo < 8) {
        return Gettext.ngettext(
            '%d week ago',
            '%d weeks ago',
            weeksAgo
        ).format(weeksAgo);
    }
    if (yearsAgo < 1) {
        return Gettext.ngettext(
            '%d month ago',
            '%d months ago',
            monthsAgo
        ).format(monthsAgo);
    }
    return Gettext.ngettext(
        '%d year ago',
        '%d years ago',
        yearsAgo
    ).format(yearsAgo);
}

/**
 * Formats a date time string based on style parameters
 *
 * @param {GLib.DateTime | Date} time a Date object
 * @param {object} [params] style parameters for the output string
 * @param {boolean=} params.timeOnly whether the string should only contain the time (no date)
 * @param {boolean=} params.ampm whether to include the "am" or "pm" in the string
 * @returns {string}
 */
export function formatTime(time, params) {
    let date;
    // HACK: The built-in Date type sucks at timezones, which we need for the
    //       world clock; it's often more convenient though, so allow either
    //       Date or GLib.DateTime as parameter
    if (time instanceof Date)
        date = _convertJSDateToGLibDateTime(time);
    else
        date = time;

    if (!date)
        return '';

    // _localTimeZone is defined in _convertJSDateToGLibDateTime
    const now = GLib.DateTime.new_now(_localTimeZone);
    const daysAgo = now.difference(date) / (24 * 60 * 60 * 1000 * 1000);

    let format;

    if (_desktopSettings == null)
        _desktopSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.interface'});
    const clockFormat = _desktopSettings.get_string('clock-format');

    params = Params.parse(params, {
        timeOnly: false,
        ampm: true,
    });

    if (clockFormat === '24h') {
        // Show only the time if date is on today
        if (daysAgo < 1 || params.timeOnly)
            /* Translators: Time in 24h format */
            format = N_('%H\u2236%M');
        // Show the word "Yesterday" and time if date is on yesterday
        else if (daysAgo < 2)
            /* Translators: this is the word "Yesterday" followed by a
             time string in 24h format. i.e. "Yesterday, 14:30" */
            // xgettext:no-c-format
            format = N_('Yesterday, %H\u2236%M');
        // Show a week day and time if date is in the last week
        else if (daysAgo < 7)
            /* Translators: this is the week day name followed by a time
             string in 24h format. i.e. "Monday, 14:30" */
            // xgettext:no-c-format
            format = N_('%A, %H\u2236%M');
        else if (date.get_year() === now.get_year())
            /* Translators: this is the month name and day number
             followed by a time string in 24h format.
             i.e. "May 25, 14:30" */
            // xgettext:no-c-format
            format = N_('%B %-d, %H\u2236%M');
        else
            /* Translators: this is the month name, day number, year
             number followed by a time string in 24h format.
             i.e. "May 25 2012, 14:30" */
            // xgettext:no-c-format
            format = N_('%B %-d %Y, %H\u2236%M');
    } else {
        // Show only the time if date is on today
        if (daysAgo < 1 || params.timeOnly) // eslint-disable-line no-lonely-if
            /* Translators: Time in 12h format */
            format = N_('%l\u2236%M %p');
        // Show the word "Yesterday" and time if date is on yesterday
        else if (daysAgo < 2)
            /* Translators: this is the word "Yesterday" followed by a
             time string in 12h format. i.e. "Yesterday, 2:30 pm" */
            // xgettext:no-c-format
            format = N_('Yesterday, %l\u2236%M %p');
        // Show a week day and time if date is in the last week
        else if (daysAgo < 7)
            /* Translators: this is the week day name followed by a time
             string in 12h format. i.e. "Monday, 2:30 pm" */
            // xgettext:no-c-format
            format = N_('%A, %l\u2236%M %p');
        else if (date.get_year() === now.get_year())
            /* Translators: this is the month name and day number
             followed by a time string in 12h format.
             i.e. "May 25, 2:30 pm" */
            // xgettext:no-c-format
            format = N_('%B %-d, %l\u2236%M %p');
        else
            /* Translators: this is the month name, day number, year
             number followed by a time string in 12h format.
             i.e. "May 25 2012, 2:30 pm"*/
            // xgettext:no-c-format
            format = N_('%B %-d %Y, %l\u2236%M %p');
    }

    // Time in short 12h format, without the equivalent of "AM" or "PM"; used
    // when it is clear from the context
    if (!params.ampm)
        format = format.replace(/\s*%p/g, '');

    let formattedTime = date.format(Shell.util_translate_time_string(format));
    // prepend LTR-mark to colon/ratio to force a text direction on times
    return formattedTime.replace(/([:\u2236])/g, '\u200e$1');
}

/**
 * Update the timezone used by JavaScript Date objects and other
 * date utilities
 */
export function clearCachedLocalTimeZone() {
    // SpiderMonkey caches the time zone so we must explicitly clear it
    // before we can update the calendar, see
    // https://bugzilla.gnome.org/show_bug.cgi?id=678507
    System.clearDateCaches();

    _localTimeZone = GLib.TimeZone.new_local();
}
(uuay)welcomeDialog.js1	// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Config from '../misc/config.js';
import * as Main from './main.js';
import * as Dialog from './dialog.js';
import * as ModalDialog from './modalDialog.js';

/** @enum {number} */
const DialogResponse = {
    SKIP: 0,
    TAKE_TOUR: 1,
};

export const WelcomeDialog = GObject.registerClass(
class WelcomeDialog extends ModalDialog.ModalDialog {
    _init() {
        super._init({styleClass: 'welcome-dialog'});

        const appSystem = Shell.AppSystem.get_default();
        this._tourAppInfo = appSystem.lookup_app('org.gnome.Tour.desktop');

        this._buildLayout();
    }

    open() {
        if (!this._tourAppInfo)
            return false;

        return super.open();
    }

    _getOSName() {
        const prettyName = GLib.get_os_info('PRETTY_NAME');
        if (prettyName)
            return prettyName;

        const name = GLib.get_os_info('NAME');
        const version = GLib.get_os_info('VERSION');
        if (name)
            return version ? `${name} ${version}` : name;

        const [majorVersion] = Config.PACKAGE_VERSION.split('.');
        return _('GNOME %s').format(majorVersion);
    }

    _buildLayout() {
        const title = _('Welcome to %s').format(this._getOSName());
        const description = _('If you want to learn your way around, check out the tour.');
        const content = new Dialog.MessageDialogContent({title, description});

        const icon = new St.Widget({style_class: 'welcome-dialog-image'});
        content.insert_child_at_index(icon, 0);

        this.contentLayout.add_child(content);

        this.addButton({
            label: _('Skip'),
            action: () => this._sendResponse(DialogResponse.SKIP),
            key: Clutter.KEY_Escape,
        });
        this.addButton({
            label: _('Take Tour'),
            action: () => this._sendResponse(DialogResponse.TAKE_TOUR),
        });
    }

    _sendResponse(response) {
        if (response === DialogResponse.TAKE_TOUR) {
            this._tourAppInfo.launch(0, -1, Shell.AppLaunchGpu.APP_PREF);
            Main.overview.hide();
        }

        this.close();
    }
});
(uuay)extensions/uWmisc/��Y/=kj{��v�S1V�%#h�xgnomeSession.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';

import {loadInterfaceXML} from './fileUtils.js';

const PresenceIface = loadInterfaceXML('org.gnome.SessionManager.Presence');

/** @enum {number} */
export const PresenceStatus = {
    AVAILABLE: 0,
    INVISIBLE: 1,
    BUSY: 2,
    IDLE: 3,
};

const PresenceProxy = Gio.DBusProxy.makeProxyWrapper(PresenceIface);

/**
 * @param {Function} initCallback
 * @param {Gio.Cancellable} cancellable
 * @returns {Gio.DBusProxy}
 */
export function Presence(initCallback, cancellable) {
    return new PresenceProxy(Gio.DBus.session,
        'org.gnome.SessionManager',
        '/org/gnome/SessionManager/Presence',
        initCallback, cancellable);
}

// Note inhibitors are immutable objects, so they don't
// change at runtime (changes always come in the form
// of new inhibitors)
const InhibitorIface = loadInterfaceXML('org.gnome.SessionManager.Inhibitor');
const InhibitorProxy = Gio.DBusProxy.makeProxyWrapper(InhibitorIface);

/**
 * @param {string} objectPath
 * @param {Function} initCallback
 * @param {Gio.Cancellable} cancellable
 * @returns {Gio.DBusProxy}
 */
export function Inhibitor(objectPath, initCallback, cancellable) {
    return new InhibitorProxy(Gio.DBus.session, 'org.gnome.SessionManager', objectPath, initCallback, cancellable);
}

// Not the full interface, only the methods we use
const SessionManagerIface = loadInterfaceXML('org.gnome.SessionManager');
const SessionManagerProxy = Gio.DBusProxy.makeProxyWrapper(SessionManagerIface);

/**
 * @param {Function} initCallback
 * @param {Gio.Cancellable} cancellable
 * @returns {Gio.DBusProxy}
 */
export function SessionManager(initCallback, cancellable) {
    return new SessionManagerProxy(Gio.DBus.session, 'org.gnome.SessionManager', '/org/gnome/SessionManager', initCallback, cancellable);
}

export const InhibitFlags = {
    LOGOUT: 1 << 0,
    SWITCH: 1 << 1,
    SUSPEND: 1 << 2,
    IDLE: 1 << 3,
    AUTOMOUNT: 1 << 4,
};
(uuay)unlockDialog.jsTy// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import AccountsService from 'gi://AccountsService';
import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import Gdm from 'gi://Gdm';
import Gio from 'gi://Gio';
import GnomeDesktop from 'gi://GnomeDesktop';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Background from './background.js';
import * as Layout from './layout.js';
import * as Main from './main.js';
import * as MessageTray from './messageTray.js';
import * as SwipeTracker from './swipeTracker.js';
import {formatDateWithCFormatString} from '../misc/dateUtils.js';
import * as AuthMenuButton from '../gdm/authMenuButton.js';
import * as AuthPrompt from '../gdm/authPrompt.js';

// The timeout before going back automatically to the lock screen (in seconds)
const IDLE_TIMEOUT = 2 * 60;

// The timeout before showing the unlock hint (in seconds)
const HINT_TIMEOUT = 4;

const CROSSFADE_TIME = 300;
const FADE_OUT_TRANSLATION = 200;
const FADE_OUT_SCALE = 0.3;

const BLUR_BRIGHTNESS = 0.65;
const BLUR_RADIUS = 90;

const NotificationsBox = GObject.registerClass({
    Signals: {'wake-up-screen': {}},
}, class NotificationsBox extends St.BoxLayout {
    _init() {
        super._init({
            vertical: true,
            name: 'unlockDialogNotifications',
        });

        this._notificationBox = new St.BoxLayout({
            vertical: true,
            style_class: 'unlock-dialog-notifications-container',
        });

        this._scrollView = new St.ScrollView({
            child: this._notificationBox,
        });
        this.add_child(this._scrollView);

        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.notifications',
        });

        this._sources = new Map();
        Main.messageTray.getSources().forEach(source => {
            this._sourceAdded(Main.messageTray, source, true);
        });
        this._updateVisibility();

        Main.messageTray.connectObject('source-added',
            this._sourceAdded.bind(this), this);

        this.connect('destroy', this._onDestroy.bind(this));
    }

    _onDestroy() {
        let items = this._sources.entries();
        for (let [source, obj] of items)
            this._removeSource(source, obj);
    }

    _updateVisibility() {
        this._notificationBox.visible =
            this._notificationBox.get_children().some(a => a.visible);

        this.visible = this._notificationBox.visible;
    }

    _makeNotificationSource(source, box) {
        let iconActor = new St.Icon({
            style_class: 'unlock-dialog-notification-icon',
            fallback_icon_name: 'application-x-executable',
        });
        source.bind_property('icon', iconActor, 'gicon', GObject.BindingFlags.SYNC_CREATE);
        box.add_child(iconActor);

        let textBox = new St.BoxLayout({
            x_expand: true,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(textBox);

        let title = new St.Label({
            text: source.title,
            style_class: 'unlock-dialog-notification-label',
            x_expand: true,
            x_align: Clutter.ActorAlign.START,
        });
        textBox.add_child(title);

        let count = source.unseenCount;
        let countLabel = new St.Label({
            text: `${count}`,
            visible: count > 1,
            style_class: 'unlock-dialog-notification-count-text',
        });
        textBox.add_child(countLabel);

        box.visible = count !== 0;
        return [title, countLabel];
    }

    _makeNotificationDetailedSource(source, box) {
        let iconActor = new St.Icon({
            style_class: 'unlock-dialog-notification-icon',
            fallback_icon_name: 'application-x-executable',
        });
        source.bind_property('icon', iconActor, 'gicon', GObject.BindingFlags.SYNC_CREATE);
        box.add_child(iconActor);

        let textBox = new St.BoxLayout({vertical: true});
        box.add_child(textBox);

        let title = new St.Label({
            text: source.title.replace(/\n/g, ' '),
            style_class: 'unlock-dialog-notification-label',
        });
        textBox.add_child(title);

        let visible = false;
        for (let i = 0; i < source.notifications.length; i++) {
            let n = source.notifications[i];

            if (n.acknowledged)
                continue;

            let body = '';
            if (n.body) {
                const bodyText = n.body.replace(/\n/g, ' ');
                body = n.useBodyMarkup
                    ? bodyText
                    : GLib.markup_escape_text(bodyText, -1);
            }

            let label = new St.Label({style_class: 'unlock-dialog-notification-count-text'});
            label.clutter_text.set_markup(`<b>${n.title}</b> ${body}`);
            textBox.add_child(label);

            visible = true;
        }

        box.visible = visible;
        return [title, null];
    }

    _shouldShowDetails(source) {
        return source.policy.detailsInLockScreen ||
               source.narrowestPrivacyScope === MessageTray.PrivacyScope.SYSTEM;
    }

    _updateSourceBoxStyle(source, obj, box) {
        let hasCriticalNotification =
            source.notifications.some(n => n.urgency === MessageTray.Urgency.CRITICAL);

        if (hasCriticalNotification !== obj.hasCriticalNotification) {
            obj.hasCriticalNotification = hasCriticalNotification;

            if (hasCriticalNotification)
                box.add_style_class_name('critical');
            else
                box.remove_style_class_name('critical');
        }
    }

    _showSource(source, obj, box) {
        if (obj.detailed)
            [obj.titleLabel, obj.countLabel] = this._makeNotificationDetailedSource(source, box);
        else
            [obj.titleLabel, obj.countLabel] = this._makeNotificationSource(source, box);

        box.visible = obj.visible && (source.unseenCount > 0);

        this._updateSourceBoxStyle(source, obj, box);
    }

    _wakeUpScreenForSource(source) {
        if (!this._settings.get_boolean('show-banners'))
            return;
        const obj = this._sources.get(source);
        if (obj?.sourceBox.visible)
            this.emit('wake-up-screen');
    }

    _sourceAdded(tray, source, initial) {
        let obj = {
            visible: source.policy.showInLockScreen,
            detailed: this._shouldShowDetails(source),
            sourceBox: null,
            titleLabel: null,
            countLabel: null,
            hasCriticalNotification: false,
        };

        obj.sourceBox = new St.BoxLayout({
            style_class: 'unlock-dialog-notification-source',
            x_expand: true,
        });
        this._showSource(source, obj, obj.sourceBox);
        this._notificationBox.add_child(obj.sourceBox);

        source.connectObject(
            'notify::count', () => this._countChanged(source, obj),
            'notify::title', () => this._titleChanged(source, obj),
            'destroy', () => {
                this._removeSource(source, obj);
                this._updateVisibility();
            }, this);
        obj.policyChangedId = source.policy.connect('notify', (policy, pspec) => {
            if (pspec.name === 'show-in-lock-screen')
                this._visibleChanged(source, obj);
            else
                this._detailedChanged(source, obj);
        });

        this._sources.set(source, obj);

        if (!initial) {
            // block scrollbars while animating, if they're not needed now
            let boxHeight = this._notificationBox.height;
            if (this._scrollView.height >= boxHeight)
                this._scrollView.vscrollbar_policy = St.PolicyType.NEVER;

            let widget = obj.sourceBox;
            let [, natHeight] = widget.get_preferred_height(-1);
            widget.height = 0;
            widget.ease({
                height: natHeight,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                duration: 250,
                onComplete: () => {
                    this._scrollView.vscrollbar_policy = St.PolicyType.AUTOMATIC;
                    widget.set_height(-1);
                },
            });

            this._updateVisibility();
            this._wakeUpScreenForSource(source);
        }
    }

    _titleChanged(source, obj) {
        obj.titleLabel.text = source.title;
    }

    _countChanged(source, obj) {
        // A change in the number of notifications may change whether we show
        // details.
        let newDetailed = this._shouldShowDetails(source);
        let oldDetailed = obj.detailed;

        obj.detailed = newDetailed;

        if (obj.detailed || oldDetailed !== newDetailed) {
            // A new notification was pushed, or a previous notification was destroyed.
            // Give up, and build the list again.

            obj.sourceBox.destroy_all_children();
            obj.titleLabel = obj.countLabel = null;
            this._showSource(source, obj, obj.sourceBox);
        } else {
            let count = source.unseenCount;
            obj.countLabel.text = `${count}`;
            obj.countLabel.visible = count > 1;
        }

        obj.sourceBox.visible = obj.visible && (source.unseenCount > 0);

        this._updateVisibility();
        this._wakeUpScreenForSource(source);
    }

    _visibleChanged(source, obj) {
        if (obj.visible === source.policy.showInLockScreen)
            return;

        obj.visible = source.policy.showInLockScreen;
        obj.sourceBox.visible = obj.visible && source.unseenCount > 0;

        this._updateVisibility();
        this._wakeUpScreenForSource(source);
    }

    _detailedChanged(source, obj) {
        let newDetailed = this._shouldShowDetails(source);
        if (obj.detailed === newDetailed)
            return;

        obj.detailed = newDetailed;

        obj.sourceBox.destroy_all_children();
        obj.titleLabel = obj.countLabel = null;
        this._showSource(source, obj, obj.sourceBox);
    }

    _removeSource(source, obj) {
        obj.sourceBox.destroy();
        obj.sourceBox = obj.titleLabel = obj.countLabel = null;

        source.policy.disconnect(obj.policyChangedId);

        this._sources.delete(source);
    }
});

const Clock = GObject.registerClass(
class UnlockDialogClock extends St.BoxLayout {
    _init() {
        super._init({style_class: 'unlock-dialog-clock', vertical: true});

        this._time = new St.Label({
            style_class: 'unlock-dialog-clock-time',
            x_align: Clutter.ActorAlign.CENTER,
        });
        this._date = new St.Label({
            style_class: 'unlock-dialog-clock-date',
            x_align: Clutter.ActorAlign.CENTER,
        });
        this._hint = new St.Label({
            style_class: 'unlock-dialog-clock-hint',
            x_align: Clutter.ActorAlign.CENTER,
            opacity: 0,
        });

        this.add_child(this._time);
        this.add_child(this._date);
        this.add_child(this._hint);

        this._wallClock = new GnomeDesktop.WallClock({time_only: true});
        this._wallClock.connect('notify::clock', this._updateClock.bind(this));

        this._seat = Clutter.get_default_backend().get_default_seat();
        this._seat.connectObject('notify::touch-mode',
            this._updateHint.bind(this), this);

        this._monitorManager = global.backend.get_monitor_manager();
        this._monitorManager.connectObject('power-save-mode-changed',
            () => (this._hint.opacity = 0), this);

        this._idleMonitor = global.backend.get_core_idle_monitor();
        this._idleWatchId = this._idleMonitor.add_idle_watch(HINT_TIMEOUT * 1000, () => {
            this._hint.ease({
                opacity: 255,
                duration: CROSSFADE_TIME,
            });
        });

        this._updateClock();
        this._updateHint();

        this.connect('destroy', this._onDestroy.bind(this));
    }

    _updateClock() {
        this._time.text = this._wallClock.clock;

        let date = new Date();
        /* Translators: This is a time format for a date in
           long format */
        let dateFormat = Shell.util_translate_time_string(N_('%A %B %-d'));
        this._date.text = formatDateWithCFormatString(date, dateFormat);
    }

    _updateHint() {
        this._hint.text = this._seat.touch_mode
            ? _('Swipe up to unlock')
            : _('Click or press a key to unlock');
    }

    _onDestroy() {
        this._wallClock.run_dispose();

        this._idleMonitor.remove_watch(this._idleWatchId);
    }
});

const UnlockDialogLayout = GObject.registerClass(
class UnlockDialogLayout extends Clutter.LayoutManager {
    _init(stack, notifications, menuButtons) {
        super._init();

        this._stack = stack;
        this._notifications = notifications;
        this._menuButtons = menuButtons;
    }

    vfunc_get_preferred_width(container, forHeight) {
        return this._stack.get_preferred_width(forHeight);
    }

    vfunc_get_preferred_height(container, forWidth) {
        return this._stack.get_preferred_height(forWidth);
    }

    vfunc_allocate(container, box) {
        let [width, height] = box.get_size();

        let tenthOfHeight = height / 10.0;
        let thirdOfHeight = height / 3.0;

        let [, , stackWidth, stackHeight] =
            this._stack.get_preferred_size();

        let [, , notificationsWidth, notificationsHeight] =
            this._notifications.get_preferred_size();

        let columnWidth = Math.max(stackWidth, notificationsWidth);

        let columnX1 = Math.floor((width - columnWidth) / 2.0);
        let actorBox = new Clutter.ActorBox();

        // Notifications
        let maxNotificationsHeight = Math.min(
            notificationsHeight,
            height - tenthOfHeight - stackHeight);

        actorBox.x1 = columnX1;
        actorBox.y1 = height - maxNotificationsHeight;
        actorBox.x2 = columnX1 + columnWidth;
        actorBox.y2 = actorBox.y1 + maxNotificationsHeight;

        this._notifications.allocate(actorBox);

        // Authentication Box
        let stackY = Math.min(
            thirdOfHeight,
            height - stackHeight - maxNotificationsHeight);

        actorBox.x1 = columnX1;
        actorBox.y1 = stackY;
        actorBox.x2 = columnX1 + columnWidth;
        actorBox.y2 = stackY + stackHeight;

        this._stack.allocate(actorBox);

        // Switch User and Login Options buttons
        if (this._menuButtons.visible) {
            let [, , natWidth, natHeight] =
                this._menuButtons.get_preferred_size();

            const textDirection = this._menuButtons.get_text_direction();
            if (textDirection === Clutter.TextDirection.RTL)
                actorBox.x1 = box.x1 + natWidth;
            else
                actorBox.x1 = box.x2 - natWidth;

            actorBox.y1 = box.y2 - natHeight;
            actorBox.x2 = actorBox.x1 + natWidth;
            actorBox.y2 = actorBox.y1 + natHeight;

            this._menuButtons.allocate(actorBox);
        }
    }
});

export const UnlockDialog = GObject.registerClass({
    Signals: {
        'failed': {},
        'wake-up-screen': {},
    },
}, class UnlockDialog extends St.Widget {
    _init(parentActor) {
        super._init({
            accessible_role: Atk.Role.WINDOW,
            style_class: 'unlock-dialog',
            visible: false,
            reactive: true,
        });

        parentActor.add_child(this);

        this._gdmClient = new Gdm.Client();

        try {
            this._gdmClient.set_enabled_extensions([
                Gdm.UserVerifierChoiceList.interface_info().name,
                Gdm.UserVerifierCustomJSON.interface_info().name,
            ]);
        } catch (e) {
        }

        this._adjustment = new St.Adjustment({
            actor: this,
            lower: 0,
            upper: 2,
            page_size: 1,
            page_increment: 1,
        });
        this._adjustment.connect('notify::value', () => {
            this._setTransitionProgress(this._adjustment.value);
        });

        this._swipeTracker = new SwipeTracker.SwipeTracker(this,
            Clutter.Orientation.VERTICAL,
            Shell.ActionMode.UNLOCK_SCREEN);
        this._swipeTracker.connect('begin', this._swipeBegin.bind(this));
        this._swipeTracker.connect('update', this._swipeUpdate.bind(this));
        this._swipeTracker.connect('end', this._swipeEnd.bind(this));

        this.connect('scroll-event', (o, event) => {
            if (this._swipeTracker.canHandleScrollEvent(event))
                return Clutter.EVENT_PROPAGATE;

            let direction = event.get_scroll_direction();
            if (direction === Clutter.ScrollDirection.UP)
                this._showClock();
            else if (direction === Clutter.ScrollDirection.DOWN)
                this._showPrompt();
            return Clutter.EVENT_STOP;
        });

        this._activePage = null;

        let tapAction = new Clutter.TapAction();
        tapAction.connect('tap', this._showPrompt.bind(this));
        this.add_action(tapAction);

        // Background
        this._backgroundGroup = new Clutter.Actor();
        this.add_child(this._backgroundGroup);

        this._bgManagers = [];

        const themeContext = St.ThemeContext.get_for_stage(global.stage);
        themeContext.connectObject('notify::scale-factor',
            () => this._updateBackgroundEffects(), this);

        this._updateBackgrounds();
        Main.layoutManager.connectObject('monitors-changed',
            this._updateBackgrounds.bind(this), this);

        this._userManager = AccountsService.UserManager.get_default();
        this._userName = GLib.get_user_name();
        this._user = this._userManager.get_user(this._userName);

        // Authentication & Clock stack
        this._stack = new Shell.Stack();

        this._promptBox = new St.BoxLayout({vertical: true});
        this._promptBox.set_pivot_point(0.5, 0.5);
        this._promptBox.hide();
        this._stack.add_child(this._promptBox);

        this._clock = new Clock();
        this._clock.set_pivot_point(0.5, 0.5);
        this._stack.add_child(this._clock);
        this._showClock();

        this.allowCancel = false;

        Main.ctrlAltTabManager.addGroup(this, _('Unlock Window'), 'dialog-password-symbolic');

        // Notifications
        this._notificationsBox = new NotificationsBox();
        this._notificationsBox.connect('wake-up-screen', () => this.emit('wake-up-screen'));

        this._menuButtons = new St.BoxLayout({
            opacity: 0,
            style_class: 'login-dialog-menu-button-box',
            x_align: Clutter.ActorAlign.END,
            y_align: Clutter.ActorAlign.END,
        });

        // Switch User button
        this._otherUserButton = new St.Button({
            style_class: 'login-dialog-button switch-user-button',
            accessible_name: _('Log in as another user'),
            button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
            reactive: false,
            x_align: Clutter.ActorAlign.END,
            y_align: Clutter.ActorAlign.END,
            icon_name: 'system-users-symbolic',
        });
        this._otherUserButton.set_pivot_point(0.5, 0.5);
        this._otherUserButton.connect('clicked', this._otherUserClicked.bind(this));
        this._menuButtons.add_child(this._otherUserButton);

        // Login Options button
        this._loginOptionsButton = new AuthMenuButton.AuthMenuButton({
            title: _('Login Options'),
            iconName: 'dialog-password-symbolic',
        });
        this._loginOptionsButton.connect('active-item-changed',
            () => {
                const activeMechanism = this._loginOptionsButton.getActiveItem();
                if (activeMechanism)
                    this._authPrompt.setForegroundMechanism(activeMechanism);
            });
        this._loginOptionsButton.updateSensitivity(true);
        this._loginOptionsButton.opacity = 255;
        this._loginOptionsButton.visible = true;
        this._menuButtons.add_child(this._loginOptionsButton);

        this._screenSaverSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.screensaver'});

        this._screenSaverSettings.connectObject('changed::user-switch-enabled',
            this._updateUserSwitchVisibility.bind(this), this);

        this._lockdownSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.lockdown'});
        this._lockdownSettings.connect('changed::disable-user-switching',
            this._updateUserSwitchVisibility.bind(this));

        this._user.connectObject(
            'notify::is-loaded', () => this._updateUserSwitchVisibility(),
            'notify::has-multiple-users', () => this._updateUserSwitchVisibility(),
            this);

        this._updateUserSwitchVisibility();

        // Main Box
        let mainBox = new St.Widget();
        mainBox.add_constraint(new Layout.MonitorConstraint({primary: true}));
        mainBox.add_child(this._stack);
        mainBox.add_child(this._notificationsBox);
        mainBox.add_child(this._menuButtons);
        mainBox.layout_manager = new UnlockDialogLayout(
            this._stack,
            this._notificationsBox,
            this._menuButtons);
        this.add_child(mainBox);

        this._idleMonitor = global.backend.get_core_idle_monitor();
        this._idleWatchId = this._idleMonitor.add_idle_watch(IDLE_TIMEOUT * 1000, this._escape.bind(this));

        this.connect('destroy', this._onDestroy.bind(this));
    }

    vfunc_key_press_event(event) {
        if (this._activePage === this._promptBox ||
            (this._promptBox && this._promptBox.visible))
            return Clutter.EVENT_PROPAGATE;

        const keyval = event.get_key_symbol();
        if (keyval === Clutter.KEY_Shift_L ||
            keyval === Clutter.KEY_Shift_R ||
            keyval === Clutter.KEY_Shift_Lock ||
            keyval === Clutter.KEY_Caps_Lock)
            return Clutter.EVENT_PROPAGATE;

        let unichar = event.get_key_unicode();

        this._showPrompt();

        if (GLib.unichar_isgraph(unichar))
            this._authPrompt.addCharacter(unichar);

        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_captured_event(event) {
        if (Main.keyboard.maybeHandleEvent(event))
            return Clutter.EVENT_STOP;

        return Clutter.EVENT_PROPAGATE;
    }

    _createBackground(monitorIndex) {
        let monitor = Main.layoutManager.monitors[monitorIndex];
        let widget = new St.Widget({
            style_class: 'screen-shield-background',
            x: monitor.x,
            y: monitor.y,
            width: monitor.width,
            height: monitor.height,
            effect: new Shell.BlurEffect({name: 'blur'}),
        });

        let bgManager = new Background.BackgroundManager({
            container: widget,
            monitorIndex,
            controlPosition: false,
        });

        this._bgManagers.push(bgManager);

        this._backgroundGroup.add_child(widget);
    }

    _updateBackgroundEffects() {
        const themeContext = St.ThemeContext.get_for_stage(global.stage);

        for (const widget of this._backgroundGroup) {
            const effect = widget.get_effect('blur');

            if (effect) {
                effect.set({
                    brightness: BLUR_BRIGHTNESS,
                    radius: BLUR_RADIUS * themeContext.scale_factor,
                });
            }
        }
    }

    _updateBackgrounds() {
        for (let i = 0; i < this._bgManagers.length; i++)
            this._bgManagers[i].destroy();

        this._bgManagers = [];
        this._backgroundGroup.destroy_all_children();

        for (let i = 0; i < Main.layoutManager.monitors.length; i++)
            this._createBackground(i);
        this._updateBackgroundEffects();
    }

    _ensureAuthPrompt() {
        if (!this._authPrompt) {
            this._authPrompt = new AuthPrompt.AuthPrompt(this._gdmClient,
                AuthPrompt.AuthPromptMode.UNLOCK_ONLY);
            this._authPrompt.connect('failed', this._fail.bind(this));
            this._authPrompt.connect('cancelled', this._fail.bind(this));
            this._authPrompt.connect('reset', this._onReset.bind(this));
            this._authPrompt.connect('mechanisms-changed', this._onMechanismsChanged.bind(this));
            this._promptBox.add_child(this._authPrompt);
        }

        this._authPrompt.reset();
        this._authPrompt.updateSensitivity(true);
    }

    _maybeDestroyAuthPrompt() {
        let focus = global.stage.key_focus;
        if (focus === null ||
            (this._authPrompt && this._authPrompt.contains(focus)) ||
            (this._otherUserButton && focus === this._otherUserButton))
            this.grab_key_focus();

        if (this._authPrompt) {
            this._authPrompt.destroy();
            this._authPrompt = null;
        }
    }

    _showClock() {
        if (this._activePage === this._clock)
            return;

        this._activePage = this._clock;

        this._adjustment.ease(0, {
            duration: CROSSFADE_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this._maybeDestroyAuthPrompt(),
        });
    }

    _showPrompt() {
        this._ensureAuthPrompt();

        if (this._activePage === this._promptBox)
            return;

        this._activePage = this._promptBox;

        this._adjustment.ease(1, {
            duration: CROSSFADE_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    _setTransitionProgress(progress) {
        this._promptBox.visible = progress > 0;
        this._clock.visible = progress < 1;

        this._otherUserButton.set({
            reactive: progress > 0,
            can_focus: progress > 0,
        });

        const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);

        this._promptBox.set({
            opacity: 255 * progress,
            scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress,
            scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress,
            translation_y: FADE_OUT_TRANSLATION * (1 - progress) * scaleFactor,
        });

        this._clock.set({
            opacity: 255 * (1 - progress),
            scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * (1 - progress),
            scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * (1 - progress),
            translation_y: -FADE_OUT_TRANSLATION * progress * scaleFactor,
        });

        this._menuButtons.set({
            opacity: 255 * progress,
            scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress,
            scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress,
        });
    }

    _fail() {
        this._showClock();
        this.emit('failed');
    }

    _onReset(authPrompt, beginRequest) {
        let userName;
        if (beginRequest !== AuthPrompt.BeginRequestType.DONT_PROVIDE_USERNAME) {
            this._authPrompt.setUser(this._user);
            userName = this._userName;
        } else {
            userName = null;
        }

        this._authPrompt.begin({userName});
    }

    _onMechanismsChanged(authPrompt, serviceName) {
        const mechanisms = Array.from(authPrompt.mechanisms.get(serviceName));

        const activeMechanism = this._loginOptionsButton.getActiveItem();

        this._loginOptionsButton.clearItems({serviceName});
        this._loginOptionsButton.updateSensitivity(mechanisms.length !== 0);
        if (mechanisms.length === 0)
            return;

        const defaultId = mechanisms[0].id;

        mechanisms.sort((a, b) => a.name.localeCompare(b.name));

        for (const {role, id, name, selectable, protocol} of mechanisms) {
            if (!selectable)
                continue;

            const mechanism = {serviceName, id, name, role, protocol};
            this._loginOptionsButton.addItem(mechanism);

            const wasActive = activeMechanism?.id === id;
            const isDefault = id === defaultId;

            if (wasActive || (!activeMechanism && isDefault))
                this._loginOptionsButton.setActiveItem(mechanism);
        }
    }

    _escape() {
        if (this._authPrompt && this.allowCancel)
            this._authPrompt.cancel();
    }

    _swipeBegin(tracker, monitor) {
        if (monitor !== Main.layoutManager.primaryIndex)
            return;

        this._adjustment.remove_transition('value');

        this._ensureAuthPrompt();

        let progress = this._adjustment.value;
        tracker.confirmSwipe(this._stack.height,
            [0, 1],
            progress,
            Math.round(progress));
    }

    _swipeUpdate(tracker, progress) {
        this._adjustment.value = progress;
    }

    _swipeEnd(tracker, duration, endProgress) {
        this._activePage = endProgress
            ? this._promptBox
            : this._clock;

        this._adjustment.ease(endProgress, {
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            duration,
            onComplete: () => {
                if (this._activePage === this._clock)
                    this._maybeDestroyAuthPrompt();
            },
        });
    }

    _otherUserClicked() {
        Gdm.goto_login_session_sync(null);

        this._authPrompt.cancel();
    }

    _onDestroy() {
        this.popModal();

        if (this._idleWatchId) {
            this._idleMonitor.remove_watch(this._idleWatchId);
            this._idleWatchId = 0;
        }

        if (this._gdmClient) {
            this._gdmClient = null;
            delete this._gdmClient;
        }
    }

    _updateUserSwitchVisibility() {
        this._otherUserButton.visible = this._userManager.can_switch() &&
            this._userManager.has_multiple_users &&
            this._screenSaverSettings.get_boolean('user-switch-enabled') &&
            !this._lockdownSettings.get_boolean('disable-user-switching');
    }

    cancel() {
        if (this._authPrompt)
            this._authPrompt.cancel();
    }

    finish(onComplete) {
        if (!this._authPrompt) {
            onComplete();
            return;
        }

        this._authPrompt.finish(onComplete);
    }

    open() {
        this.show();

        if (this._isModal)
            return true;

        const grab = Main.pushModal(Main.uiGroup,
            {actionMode: Shell.ActionMode.UNLOCK_SCREEN});
        if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
            Main.popModal(grab);
            return false;
        }

        this._grab = grab;
        this._isModal = true;

        return true;
    }

    activate() {
        this._showPrompt();
    }

    popModal() {
        if (this._isModal) {
            Main.popModal(this._grab);
            this._grab = null;
            this._isModal = false;
        }
    }
});
(uuay)brightness.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GObject from 'gi://GObject';

import {QuickSlider, SystemIndicator} from '../quickSettings.js';

import {loadInterfaceXML} from '../../misc/fileUtils.js';

const BUS_NAME = 'org.gnome.SettingsDaemon.Power';
const OBJECT_PATH = '/org/gnome/SettingsDaemon/Power';

const BrightnessInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Power.Screen');
const BrightnessProxy = Gio.DBusProxy.makeProxyWrapper(BrightnessInterface);

const BrightnessItem = GObject.registerClass(
class BrightnessItem extends QuickSlider {
    _init() {
        super._init({
            iconName: 'display-brightness-symbolic',
        });

        this._proxy = new BrightnessProxy(Gio.DBus.session, BUS_NAME, OBJECT_PATH,
            (proxy, error) => {
                if (error)
                    console.error(error.message);
                else
                    this._proxy.connect('g-properties-changed', () => this._sync());
                this._sync();
            });

        this._sliderChangedId = this.slider.connect('notify::value',
            this._sliderChanged.bind(this));
        this.slider.accessible_name = _('Brightness');
    }

    _sliderChanged() {
        const percent = this.slider.value * 100;
        this._proxy.Brightness = percent;
    }

    _changeSlider(value) {
        this.slider.block_signal_handler(this._sliderChangedId);
        this.slider.value = value;
        this.slider.unblock_signal_handler(this._sliderChangedId);
    }

    _sync() {
        const brightness = this._proxy.Brightness;
        const visible = Number.isInteger(brightness) && brightness >= 0;
        this.visible = visible;
        if (visible)
            this._changeSlider(this._proxy.Brightness / 100.0);
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this.quickSettingsItems.push(new BrightnessItem());
    }
});
(uuay)accessibility.jsI// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St';

import * as PanelMenu from '../panelMenu.js';
import * as PopupMenu from '../popupMenu.js';

const A11Y_SCHEMA                   = 'org.gnome.desktop.a11y';
const KEY_ALWAYS_SHOW               = 'always-show-universal-access-status';

const A11Y_KEYBOARD_SCHEMA          = 'org.gnome.desktop.a11y.keyboard';
const KEY_STICKY_KEYS_ENABLED       = 'stickykeys-enable';
const KEY_BOUNCE_KEYS_ENABLED       = 'bouncekeys-enable';
const KEY_SLOW_KEYS_ENABLED         = 'slowkeys-enable';
const KEY_MOUSE_KEYS_ENABLED        = 'mousekeys-enable';

const APPLICATIONS_SCHEMA           = 'org.gnome.desktop.a11y.applications';

const DPI_FACTOR_LARGE              = 1.25;

const WM_SCHEMA                     = 'org.gnome.desktop.wm.preferences';
const KEY_VISUAL_BELL               = 'visual-bell';

const DESKTOP_INTERFACE_SCHEMA      = 'org.gnome.desktop.interface';
const KEY_TEXT_SCALING_FACTOR       = 'text-scaling-factor';

const A11Y_INTERFACE_SCHEMA         = 'org.gnome.desktop.a11y.interface';
const KEY_HIGH_CONTRAST             = 'high-contrast';

export const ATIndicator = GObject.registerClass(
class ATIndicator extends PanelMenu.Button {
    _init() {
        super._init(0.5, _('Accessibility'));

        this.add_child(new St.Icon({
            style_class: 'system-status-icon',
            icon_name: 'org.gnome.Settings-accessibility-symbolic',
        }));

        this._a11ySettings = new Gio.Settings({schema_id: A11Y_SCHEMA});
        this._a11ySettings.connect(`changed::${KEY_ALWAYS_SHOW}`, this._queueSyncMenuVisibility.bind(this));

        let highContrast = this._buildItem(_('High Contrast'), A11Y_INTERFACE_SCHEMA, KEY_HIGH_CONTRAST);
        this.menu.addMenuItem(highContrast);

        const magnifier = this._buildItem(_('Zoom'),
            APPLICATIONS_SCHEMA, 'screen-magnifier-enabled');
        this.menu.addMenuItem(magnifier);

        let textZoom = this._buildFontItem();
        this.menu.addMenuItem(textZoom);

        const screenReader = this._buildItem(_('Screen Reader'),
            APPLICATIONS_SCHEMA, 'screen-reader-enabled');
        this.menu.addMenuItem(screenReader);

        const screenKeyboard = this._buildItem(_('Screen Keyboard'),
            APPLICATIONS_SCHEMA, 'screen-keyboard-enabled');
        this.menu.addMenuItem(screenKeyboard);

        const visualBell = this._buildItem(_('Visual Alerts'),
            WM_SCHEMA, KEY_VISUAL_BELL);
        this.menu.addMenuItem(visualBell);

        const stickyKeys = this._buildItem(_('Sticky Keys'),
            A11Y_KEYBOARD_SCHEMA, KEY_STICKY_KEYS_ENABLED);
        this.menu.addMenuItem(stickyKeys);

        const slowKeys = this._buildItem(_('Slow Keys'),
            A11Y_KEYBOARD_SCHEMA, KEY_SLOW_KEYS_ENABLED);
        this.menu.addMenuItem(slowKeys);

        const bounceKeys = this._buildItem(_('Bounce Keys'),
            A11Y_KEYBOARD_SCHEMA, KEY_BOUNCE_KEYS_ENABLED);
        this.menu.addMenuItem(bounceKeys);

        const mouseKeys = this._buildItem(_('Mouse Keys'),
            A11Y_KEYBOARD_SCHEMA, KEY_MOUSE_KEYS_ENABLED);
        this.menu.addMenuItem(mouseKeys);

        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this.menu.addSettingsAction(_('Accessibility Settings'),
            'gnome-universal-access-panel.desktop');

        this._syncMenuVisibility();
    }

    _syncMenuVisibility() {
        this._syncMenuVisibilityIdle = 0;

        let alwaysShow = this._a11ySettings.get_boolean(KEY_ALWAYS_SHOW);
        let items = this.menu._getMenuItems();

        this.visible = alwaysShow || items.some(f => !!f.state);

        return GLib.SOURCE_REMOVE;
    }

    _queueSyncMenuVisibility() {
        if (this._syncMenuVisibilityIdle)
            return;

        this._syncMenuVisibilityIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._syncMenuVisibility.bind(this));
        GLib.Source.set_name_by_id(this._syncMenuVisibilityIdle, '[gnome-shell] this._syncMenuVisibility');
    }

    _buildItemExtended(string, initialValue, writable, onSet) {
        let widget = new PopupMenu.PopupSwitchMenuItem(string, initialValue);
        if (!writable) {
            widget.reactive = false;
        } else {
            widget.connect('toggled', item => {
                onSet(item.state);
            });
        }
        return widget;
    }

    _buildItem(string, schema, key) {
        let settings = new Gio.Settings({schema_id: schema});
        let widget = this._buildItemExtended(string,
            settings.get_boolean(key),
            settings.is_writable(key),
            enabled => settings.set_boolean(key, enabled));

        settings.connect(`changed::${key}`, () => {
            widget.setToggleState(settings.get_boolean(key));

            this._queueSyncMenuVisibility();
        });

        return widget;
    }

    _buildFontItem() {
        let settings = new Gio.Settings({schema_id: DESKTOP_INTERFACE_SCHEMA});
        let factor = settings.get_double(KEY_TEXT_SCALING_FACTOR);
        let initialSetting = factor > 1.0;
        let widget = this._buildItemExtended(_('Large Text'),
            initialSetting,
            settings.is_writable(KEY_TEXT_SCALING_FACTOR),
            enabled => {
                if (enabled) {
                    settings.set_double(
                        KEY_TEXT_SCALING_FACTOR, DPI_FACTOR_LARGE);
                } else {
                    settings.reset(KEY_TEXT_SCALING_FACTOR);
                }
            });

        settings.connect(`changed::${KEY_TEXT_SCALING_FACTOR}`, () => {
            factor = settings.get_double(KEY_TEXT_SCALING_FACTOR);
            let active = factor > 1.0;
            widget.setToggleState(active);

            this._queueSyncMenuVisibility();
        });

        return widget;
    }
});
(uuay)lookingGlass.js.�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Cogl from 'gi://Cogl';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';
import System from 'system';

import * as History from '../misc/history.js';
import {ExtensionState} from '../misc/extensionUtils.js';
import * as PopupMenu from './popupMenu.js';
import * as ShellEntry from './shellEntry.js';
import * as Main from './main.js';
import * as JsParse from '../misc/jsParse.js';

const CHEVRON = '>>> ';

/* Imports...feel free to add here as needed */
const commandHeader = `
    const {Clutter, Gio, GLib, GObject, Meta, Shell, St} = imports.gi;
    const Main = await import('resource:///org/gnome/shell/ui/main.js');

    /* Utility functions...we should probably be able to use these
     * in the shell core code too. */
    const stage = global.stage;

    /* Special lookingGlass functions */
    const inspect = Main.lookingGlass.inspect.bind(Main.lookingGlass);
    const it = Main.lookingGlass.getIt();
    const r = Main.lookingGlass.getResult.bind(Main.lookingGlass);
    `;
const AsyncFunction = async function () {}.constructor;

const HISTORY_KEY = 'looking-glass-history';
// Time between tabs for them to count as a double-tab event

const AUTO_COMPLETE_DOUBLE_TAB_DELAY = 500;
const AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION = 200;
const AUTO_COMPLETE_GLOBAL_KEYWORDS = _getAutoCompleteGlobalKeywords();

const LG_ANIMATION_TIME = 500;

const CLUTTER_DEBUG_FLAG_CATEGORIES = new Map([
    // Paint debugging can easily result in a non-responsive session
    ['DebugFlag', {argPos: 0, exclude: ['PAINT']}],
    ['DrawDebugFlag', {argPos: 1, exclude: []}],
    // Exluded due to the only current option likely to result in shooting ones
    // foot
    // ['PickDebugFlag', { argPos: 2, exclude: [] }],
]);

function _getAutoCompleteGlobalKeywords() {
    const keywords = ['true', 'false', 'null', 'new'];
    // Don't add the private properties of globalThis (i.e., ones starting with '_')
    const windowProperties = Object.getOwnPropertyNames(globalThis).filter(
        a => a.charAt(0) !== '_');
    const headerProperties = JsParse.getDeclaredConstants(commandHeader);

    return keywords.concat(windowProperties).concat(headerProperties);
}

class AutoComplete extends Signals.EventEmitter {
    constructor(entry) {
        super();

        this._entry = entry;
        this._entry.connect('key-press-event', this._entryKeyPressEvent.bind(this));
        this._lastTabTime = global.get_current_time();
    }

    _processCompletionRequest(event) {
        if (event.completions.length === 0)
            return;

        // Unique match = go ahead and complete; multiple matches + single tab = complete the common starting string;
        // multiple matches + double tab = emit a suggest event with all possible options
        if (event.completions.length === 1) {
            this.additionalCompletionText(event.completions[0], event.attrHead);
            this.emit('completion', {completion: event.completions[0], type: 'whole-word'});
        } else if (event.completions.length > 1 && event.tabType === 'single') {
            let commonPrefix = JsParse.getCommonPrefix(event.completions);

            if (commonPrefix.length > 0) {
                this.additionalCompletionText(commonPrefix, event.attrHead);
                this.emit('completion', {completion: commonPrefix, type: 'prefix'});
                this.emit('suggest', {completions: event.completions});
            }
        } else if (event.completions.length > 1 && event.tabType === 'double') {
            this.emit('suggest', {completions: event.completions});
        }
    }

    async _handleCompletions(text, time) {
        const [completions, attrHead] =
            await JsParse.getCompletions(text, commandHeader, AUTO_COMPLETE_GLOBAL_KEYWORDS);

        const tabType = (time - this._lastTabTime) < AUTO_COMPLETE_DOUBLE_TAB_DELAY
            ? 'double' : 'single';

        this._processCompletionRequest({
            tabType,
            completions,
            attrHead,
        });
        this._lastTabTime = time;
    }

    _entryKeyPressEvent(actor, event) {
        let cursorPos = this._entry.clutter_text.get_cursor_position();
        let text = this._entry.get_text();
        if (cursorPos !== -1)
            text = text.slice(0, cursorPos);

        if (event.get_key_symbol() === Clutter.KEY_Tab)
            this._handleCompletions(text, event.get_time()).catch(logError);
        return Clutter.EVENT_PROPAGATE;
    }

    // Insert characters of text not already included in head at cursor position.  i.e., if text="abc" and head="a",
    // the string "bc" will be appended to this._entry
    additionalCompletionText(text, head) {
        let additionalCompletionText = text.slice(head.length);
        let cursorPos = this._entry.clutter_text.get_cursor_position();

        this._entry.clutter_text.insert_text(additionalCompletionText, cursorPos);
    }
}

const Notebook = GObject.registerClass({
    Signals: {'selection': {param_types: [Clutter.Actor.$gtype]}},
}, class Notebook extends St.BoxLayout {
    _init() {
        super._init({
            vertical: true,
            y_expand: true,
        });

        this.tabControls = new St.BoxLayout({style_class: 'labels'});

        this._selectedIndex = -1;
        this._tabs = [];
    }

    appendPage(name, child) {
        const labelBox = new St.BoxLayout({
            style_class: 'notebook-tab',
            reactive: true,
            track_hover: true,
        });
        let label = new St.Button({label: name});
        label.connect('clicked', () => {
            this.selectChild(child);
            return true;
        });
        labelBox.add_child(label);
        this.tabControls.add_child(labelBox);

        const scrollview = new St.ScrollView({
            child,
            y_expand: true,
        });

        const tabData = {
            child,
            labelBox,
            label,
            scrollView: scrollview,
            _scrollToBottom: false,
        };
        this._tabs.push(tabData);
        scrollview.hide();
        this.add_child(scrollview);

        const vAdjust = scrollview.vadjustment;
        vAdjust.connect('changed', () => this._onAdjustScopeChanged(tabData));
        vAdjust.connect('notify::value', () => this._onAdjustValueChanged(tabData));

        if (this._selectedIndex === -1)
            this.selectIndex(0);
    }

    _unselect() {
        if (this._selectedIndex < 0)
            return;
        let tabData = this._tabs[this._selectedIndex];
        tabData.labelBox.remove_style_pseudo_class('selected');
        tabData.scrollView.hide();
        this._selectedIndex = -1;
    }

    selectIndex(index) {
        if (index === this._selectedIndex)
            return;
        if (index < 0) {
            this._unselect();
            this.emit('selection', null);
            return;
        }

        // Focus the new tab before unmapping the old one
        let tabData = this._tabs[index];
        if (!tabData.scrollView.navigate_focus(null, St.DirectionType.TAB_FORWARD, false))
            this.grab_key_focus();

        this._unselect();

        tabData.labelBox.add_style_pseudo_class('selected');
        tabData.scrollView.show();
        this._selectedIndex = index;
        this.emit('selection', tabData.child);
    }

    selectChild(child) {
        if (child == null) {
            this.selectIndex(-1);
        } else {
            for (let i = 0; i < this._tabs.length; i++) {
                let tabData = this._tabs[i];
                if (tabData.child === child) {
                    this.selectIndex(i);
                    return;
                }
            }
        }
    }

    scrollToBottom(index) {
        let tabData = this._tabs[index];
        tabData._scrollToBottom = true;
    }

    _onAdjustValueChanged(tabData) {
        const vAdjust = tabData.scrollView.vadjustment;
        if (vAdjust.value < (vAdjust.upper - vAdjust.lower - 0.5))
            tabData._scrolltoBottom = false;
    }

    _onAdjustScopeChanged(tabData) {
        if (!tabData._scrollToBottom)
            return;
        const vAdjust = tabData.scrollView.vadjustment;
        vAdjust.value = vAdjust.upper - vAdjust.page_size;
    }

    nextTab() {
        let nextIndex = this._selectedIndex;
        if (nextIndex < this._tabs.length - 1)
            ++nextIndex;

        this.selectIndex(nextIndex);
    }

    prevTab() {
        let prevIndex = this._selectedIndex;
        if (prevIndex > 0)
            --prevIndex;

        this.selectIndex(prevIndex);
    }
});

function objectToString(o) {
    if (typeof o === typeof objectToString) {
        // special case this since the default is way, way too verbose
        return '<js function>';
    } else if (o && o.toString === undefined) {
        // eeks, something unprintable. we'll have to guess, probably a module
        return typeof o === 'object' && !(o instanceof Object)
            ? '<module>'
            : '<unknown>';
    } else {
        return `${o}`;
    }
}

const ObjLink = GObject.registerClass(
class ObjLink extends St.Button {
    _init(lookingGlass, o, title) {
        let text;
        if (title)
            text = title;
        else
            text = objectToString(o);

        super._init({
            reactive: true,
            track_hover: true,
            style_class: 'shell-link',
            label: text,
            x_align: Clutter.ActorAlign.START,
        });
        this.get_child().single_line_mode = true;

        this._obj = o;
        this._lookingGlass = lookingGlass;
    }

    vfunc_clicked() {
        this._lookingGlass.inspectObject(this._obj, this);
    }
});

const Result = GObject.registerClass(
class Result extends St.BoxLayout {
    _init(lookingGlass, command, o, index) {
        super._init({vertical: true});

        this.index = index;
        this.o = o;

        this._lookingGlass = lookingGlass;

        let cmdTxt = new St.Label({text: command});
        cmdTxt.clutter_text.ellipsize = Pango.EllipsizeMode.END;
        this.add_child(cmdTxt);
        let box = new St.BoxLayout({});
        this.add_child(box);
        let resultTxt = new St.Label({text: `r(${index}) = `});
        resultTxt.clutter_text.ellipsize = Pango.EllipsizeMode.END;
        box.add_child(resultTxt);
        let objLink = new ObjLink(this._lookingGlass, o);
        box.add_child(objLink);
    }
});

const WindowList = GObject.registerClass({
}, class WindowList extends St.BoxLayout {
    _init(lookingGlass) {
        super._init({name: 'Windows', vertical: true, style: 'spacing: 8px'});
        let tracker = Shell.WindowTracker.get_default();
        this._updateId = Main.initializeDeferredWork(this, this._updateWindowList.bind(this));
        global.display.connect('window-created', this._updateWindowList.bind(this));
        tracker.connect('tracked-windows-changed', this._updateWindowList.bind(this));

        this._lookingGlass = lookingGlass;
    }

    _updateWindowList() {
        if (!this._lookingGlass.isOpen)
            return;

        this.destroy_all_children();
        let windows = global.get_window_actors();
        let tracker = Shell.WindowTracker.get_default();
        for (let i = 0; i < windows.length; i++) {
            let metaWindow = windows[i].metaWindow;
            // Avoid multiple connections
            if (!metaWindow._lookingGlassManaged) {
                metaWindow.connect('unmanaged', this._updateWindowList.bind(this));
                metaWindow._lookingGlassManaged = true;
            }
            let box = new St.BoxLayout({vertical: true});
            this.add_child(box);
            let windowLink = new ObjLink(this._lookingGlass, metaWindow, metaWindow.title);
            box.add_child(windowLink);
            let propsBox = new St.BoxLayout({vertical: true, style: 'padding-left: 6px;'});
            box.add_child(propsBox);
            propsBox.add_child(new St.Label({text: `wmclass: ${metaWindow.get_wm_class()}`}));
            let app = tracker.get_window_app(metaWindow);
            if (app != null && !app.is_window_backed()) {
                let icon = app.create_icon_texture(22);
                let propBox = new St.BoxLayout({style: 'spacing: 6px; '});
                propsBox.add_child(propBox);
                propBox.add_child(new St.Label({text: 'app: '}));
                let appLink = new ObjLink(this._lookingGlass, app, app.get_id());
                propBox.add_child(appLink);
                propBox.add_child(icon);
            } else {
                propsBox.add_child(new St.Label({text: '<untracked>'}));
            }
        }
    }

    update() {
        this._updateWindowList();
    }
});

const ObjInspector = GObject.registerClass(
class ObjInspector extends St.ScrollView {
    _init(lookingGlass) {
        super._init({
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
        });

        this._obj = null;
        this._previousObj = null;

        this._parentList = [];

        this._container = new St.BoxLayout({
            name: 'LookingGlassPropertyInspector',
            style_class: 'lg-dialog',
            vertical: true,
            x_expand: true,
            y_expand: true,
        });
        this.child = this._container;

        this._lookingGlass = lookingGlass;
    }

    selectObject(obj, skipPrevious) {
        if (!skipPrevious)
            this._previousObj = this._obj;
        else
            this._previousObj = null;
        this._obj = obj;

        this._container.destroy_all_children();

        let hbox = new St.BoxLayout({style_class: 'lg-obj-inspector-title'});
        this._container.add_child(hbox);
        let label = new St.Label({
            text: `Inspecting: ${typeof obj}: ${objectToString(obj)}`,
            x_expand: true,
        });
        label.single_line_mode = true;
        hbox.add_child(label);
        let button = new St.Button({label: 'Insert', style_class: 'lg-obj-inspector-button'});
        button.connect('clicked', this._onInsert.bind(this));
        hbox.add_child(button);

        if (this._previousObj != null) {
            button = new St.Button({label: 'Back', style_class: 'lg-obj-inspector-button'});
            button.connect('clicked', this._onBack.bind(this));
            hbox.add_child(button);
        }

        button = new St.Button({
            style_class: 'window-close',
            icon_name: 'window-close-symbolic',
        });
        button.connect('clicked', this.close.bind(this));
        hbox.add_child(button);
        if (typeof obj === typeof {}) {
            let properties = [];
            for (let propName in obj)
                properties.push(propName);
            properties.sort();

            for (let i = 0; i < properties.length; i++) {
                let propName = properties[i];
                let link;
                try {
                    let prop = obj[propName];
                    link = new ObjLink(this._lookingGlass, prop);
                } catch (e) {
                    link = new St.Label({text: '<error>'});
                }
                let box = new St.BoxLayout();
                box.add_child(new St.Label({text: `${propName}: `}));
                box.add_child(link);
                this._container.add_child(box);
            }
        }
    }

    open(sourceActor) {
        if (this._open)
            return;

        const grab = Main.pushModal(this, {actionMode: Shell.ActionMode.LOOKING_GLASS});
        if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
            Main.popModal(grab);
            return;
        }

        this._grab = grab;
        this._previousObj = null;
        this._open = true;
        this.show();
        if (sourceActor) {
            this.set_scale(0, 0);
            this.ease({
                scale_x: 1,
                scale_y: 1,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                duration: 200,
            });
        } else {
            this.set_scale(1, 1);
        }
    }

    close() {
        if (!this._open)
            return;
        Main.popModal(this._grab);
        this._grab = null;
        this._open = false;
        this.hide();
        this._previousObj = null;
        this._obj = null;
    }

    vfunc_key_press_event(event) {
        const symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_Escape) {
            this.close();
            return Clutter.EVENT_STOP;
        }
        return super.vfunc_key_press_event(event);
    }

    _onInsert() {
        let obj = this._obj;
        this.close();
        this._lookingGlass.insertObject(obj);
    }

    _onBack() {
        this.selectObject(this._previousObj, true);
    }
});

const RedBorderEffect = GObject.registerClass(
class RedBorderEffect extends Clutter.Effect {
    _init() {
        super._init();
        this._pipeline = null;
    }

    vfunc_paint_node(node, paintContext) {
        let actor = this.get_actor();

        const actorNode = new Clutter.ActorNode(actor, -1);
        node.add_child(actorNode);

        if (!this._pipeline) {
            const framebuffer = paintContext.get_framebuffer();
            const coglContext = framebuffer.get_context();

            let color = new Cogl.Color();
            color.init_from_4f(1.0, 0.0, 0.0, 196.0 / 255.0);

            this._pipeline = Cogl.Pipeline.new(coglContext);
            this._pipeline.set_color(color);
        }

        let alloc = actor.get_allocation_box();
        let width = 2;

        const pipelineNode = new Clutter.PipelineNode(this._pipeline);
        pipelineNode.set_name('Red Border');
        node.add_child(pipelineNode);

        const box = new Clutter.ActorBox();

        // clockwise order
        box.set_origin(0, 0);
        box.set_size(alloc.get_width(), width);
        pipelineNode.add_rectangle(box);

        box.set_origin(alloc.get_width() - width, width);
        box.set_size(width, alloc.get_height() - width);
        pipelineNode.add_rectangle(box);

        box.set_origin(0, alloc.get_height() - width);
        box.set_size(alloc.get_width() - width, width);
        pipelineNode.add_rectangle(box);

        box.set_origin(0, width);
        box.set_size(width, alloc.get_height() - width * 2);
        pipelineNode.add_rectangle(box);
    }
});

export const Inspector = GObject.registerClass({
    Signals: {
        'closed': {},
        'target': {param_types: [Clutter.Actor.$gtype, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
    },
}, class Inspector extends Clutter.Actor {
    _init(lookingGlass) {
        super._init({width: 0, height: 0});

        Main.uiGroup.add_child(this);

        const eventHandler = new St.BoxLayout({
            name: 'LookingGlassDialog',
            vertical: false,
            reactive: true,
        });
        this._eventHandler = eventHandler;
        this.add_child(eventHandler);
        this._displayText = new St.Label({x_expand: true});
        eventHandler.add_child(this._displayText);

        eventHandler.connect('key-press-event', this._onKeyPressEvent.bind(this));
        eventHandler.connect('button-press-event', this._onButtonPressEvent.bind(this));
        eventHandler.connect('scroll-event', this._onScrollEvent.bind(this));
        eventHandler.connect('motion-event', this._onMotionEvent.bind(this));

        this._grab = global.stage.grab(eventHandler);

        // this._target is the actor currently shown by the inspector.
        // this._pointerTarget is the actor directly under the pointer.
        // Normally these are the same, but if you use the scroll wheel
        // to drill down, they'll diverge until you either scroll back
        // out, or move the pointer outside of _pointerTarget.
        this._target = null;
        this._pointerTarget = null;

        this._lookingGlass = lookingGlass;
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        if (!this._eventHandler)
            return;

        let primary = Main.layoutManager.primaryMonitor;

        let [, , natWidth, natHeight] =
            this._eventHandler.get_preferred_size();

        let childBox = new Clutter.ActorBox();
        childBox.x1 = primary.x + Math.floor((primary.width - natWidth) / 2);
        childBox.x2 = childBox.x1 + natWidth;
        childBox.y1 = primary.y + Math.floor((primary.height - natHeight) / 2);
        childBox.y2 = childBox.y1 + natHeight;
        this._eventHandler.allocate(childBox);
    }

    _close() {
        if (this._grab) {
            this._grab.dismiss();
            this._grab = null;
        }
        this._eventHandler.destroy();
        this._eventHandler = null;
        this.emit('closed');
    }

    _onKeyPressEvent(actor, event) {
        if (event.get_key_symbol() === Clutter.KEY_Escape)
            this._close();
        return Clutter.EVENT_STOP;
    }

    _onButtonPressEvent(actor, event) {
        if (this._target) {
            let [stageX, stageY] = event.get_coords();
            this.emit('target', this._target, stageX, stageY);
        }
        this._close();
        return Clutter.EVENT_STOP;
    }

    _onScrollEvent(actor, event) {
        switch (event.get_scroll_direction()) {
        case Clutter.ScrollDirection.UP: {
            // select parent
            let parent = this._target.get_parent();
            if (parent != null) {
                this._target = parent;
                this._update(event);
            }
            break;
        }

        case Clutter.ScrollDirection.DOWN:
            // select child
            if (this._target !== this._pointerTarget) {
                let child = this._pointerTarget;
                while (child) {
                    let parent = child.get_parent();
                    if (parent === this._target)
                        break;
                    child = parent;
                }
                if (child) {
                    this._target = child;
                    this._update(event);
                }
            }
            break;

        default:
            break;
        }
        return Clutter.EVENT_STOP;
    }

    _onMotionEvent(actor, event) {
        this._update(event);
        return Clutter.EVENT_STOP;
    }

    _update(event) {
        let [stageX, stageY] = event.get_coords();
        let target = global.stage.get_actor_at_pos(
            Clutter.PickMode.ALL, stageX, stageY);

        if (target !== this._pointerTarget)
            this._target = target;
        this._pointerTarget = target;

        let position = `[inspect x: ${stageX} y: ${stageY}]`;
        this._displayText.text = '';
        this._displayText.text = `${position} ${this._target}`;

        this._lookingGlass.setBorderPaintTarget(this._target);
    }
});

const Extensions = GObject.registerClass({
}, class Extensions extends St.BoxLayout {
    _init(lookingGlass) {
        super._init({vertical: true, name: 'lookingGlassExtensions'});

        this._lookingGlass = lookingGlass;
        this._noExtensions = new St.Label({
            style_class: 'lg-extensions-none',
            text: _('No extensions installed'),
        });
        this._numExtensions = 0;
        this._extensionsList = new St.BoxLayout({
            vertical: true,
            style_class: 'lg-extensions-list',
        });
        this._extensionsList.add_child(this._noExtensions);
        this.add_child(this._extensionsList);

        Main.extensionManager.getUuids().forEach(uuid => {
            this._loadExtension(null, uuid);
        });

        Main.extensionManager.connect('extension-loaded',
            this._loadExtension.bind(this));
    }

    _loadExtension(o, uuid) {
        let extension = Main.extensionManager.lookup(uuid);
        // There can be cases where we create dummy extension metadata
        // that's not really a proper extension. Don't bother with these.
        if (!extension.metadata.name)
            return;

        let extensionDisplay = this._createExtensionDisplay(extension);
        if (this._numExtensions === 0)
            this._extensionsList.remove_child(this._noExtensions);

        this._numExtensions++;
        const {name} = extension.metadata;
        const pos = [...this._extensionsList].findIndex(
            dsp => dsp._extension.metadata.name.localeCompare(name) > 0);
        this._extensionsList.insert_child_at_index(extensionDisplay, pos);
    }

    _onViewSource(actor) {
        let extension = actor._extension;
        let uri = extension.dir.get_uri();
        Gio.app_info_launch_default_for_uri(uri, global.create_app_launch_context(0, -1));
        this._lookingGlass.close();
    }

    _onWebPage(actor) {
        let extension = actor._extension;
        Gio.app_info_launch_default_for_uri(extension.metadata.url, global.create_app_launch_context(0, -1));
        this._lookingGlass.close();
    }

    _onViewErrors(actor) {
        let extension = actor._extension;
        let shouldShow = !actor._isShowing;

        if (shouldShow) {
            let errors = extension.errors;
            let errorDisplay = new St.BoxLayout({vertical: true});
            if (errors && errors.length) {
                for (let i = 0; i < errors.length; i++)
                    errorDisplay.add_child(new St.Label({text: errors[i]}));
            } else {
                /* Translators: argument is an extension UUID. */
                let message = _('%s has not emitted any errors.').format(extension.uuid);
                errorDisplay.add_child(new St.Label({text: message}));
            }

            actor._errorDisplay = errorDisplay;
            actor._parentBox.add_child(errorDisplay);
            actor.label = _('Hide Errors');
        } else {
            actor._errorDisplay.destroy();
            actor._errorDisplay = null;
            actor.label = _('Show Errors');
        }

        actor._isShowing = shouldShow;
    }

    _stateToString(extensionState) {
        switch (extensionState) {
        case ExtensionState.ACTIVE:
            return _('Active');
        case ExtensionState.INACTIVE:
        case ExtensionState.INITIALIZED:
            return _('Inactive');
        case ExtensionState.ERROR:
            return _('Error');
        case ExtensionState.OUT_OF_DATE:
            return _('Out of date');
        case ExtensionState.DOWNLOADING:
            return _('Downloading');
        case ExtensionState.DEACTIVATING:
            return _('Deactivating');
        case ExtensionState.ACTIVATING:
            return _('Activating');
        }
        return 'Unknown'; // Not translated, shouldn't appear
    }

    _createExtensionDisplay(extension) {
        let box = new St.BoxLayout({style_class: 'lg-extension', vertical: true});
        box._extension = extension;
        let name = new St.Label({
            style_class: 'lg-extension-name',
            text: extension.metadata.name,
            x_expand: true,
        });
        box.add_child(name);
        let description = new St.Label({
            style_class: 'lg-extension-description',
            text: extension.metadata.description || 'No description',
            x_expand: true,
        });
        box.add_child(description);

        let metaBox = new St.BoxLayout({style_class: 'lg-extension-meta'});
        box.add_child(metaBox);
        const state = new St.Label({
            style_class: 'lg-extension-state',
            text: this._stateToString(extension.state),
        });
        metaBox.add_child(state);

        const viewsource = new St.Button({
            reactive: true,
            track_hover: true,
            style_class: 'shell-link',
            label: _('View Source'),
        });
        viewsource._extension = extension;
        viewsource.connect('clicked', this._onViewSource.bind(this));
        metaBox.add_child(viewsource);

        if (extension.metadata.url) {
            const webpage = new St.Button({
                reactive: true,
                track_hover: true,
                style_class: 'shell-link',
                label: _('Web Page'),
            });
            webpage._extension = extension;
            webpage.connect('clicked', this._onWebPage.bind(this));
            metaBox.add_child(webpage);
        }

        const viewerrors = new St.Button({
            reactive: true,
            track_hover: true,
            style_class: 'shell-link',
            label: _('Show Errors'),
        });
        viewerrors._extension = extension;
        viewerrors._parentBox = box;
        viewerrors._isShowing = false;
        viewerrors.connect('clicked', this._onViewErrors.bind(this));
        metaBox.add_child(viewerrors);

        return box;
    }
});


const ActorLink = GObject.registerClass({
    Signals: {
        'inspect-actor': {},
    },
}, class ActorLink extends St.Button {
    _init(actor) {
        this._arrow = new St.Icon({
            icon_name: 'pan-end-symbolic',
            icon_size: 8,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
        });

        const label = new St.Label({
            text: actor.toString(),
            x_align: Clutter.ActorAlign.START,
        });

        const inspectButton = new St.Button({
            icon_name: 'insert-object-symbolic',
            reactive: true,
            x_expand: true,
            x_align: Clutter.ActorAlign.START,
            y_align: Clutter.ActorAlign.CENTER,
        });
        inspectButton.connect('clicked', () => this.emit('inspect-actor'));

        const box = new St.BoxLayout();
        box.add_child(this._arrow);
        box.add_child(label);
        box.add_child(inspectButton);

        super._init({
            reactive: true,
            track_hover: true,
            toggle_mode: true,
            style_class: 'actor-link',
            child: box,
            x_align: Clutter.ActorAlign.START,
        });

        this._actor = actor;
    }

    vfunc_clicked() {
        this._arrow.ease({
            rotation_angle_z: this.checked ? 90 : 0,
            duration: 250,
        });
    }
});

const ActorTreeViewer = GObject.registerClass(
class ActorTreeViewer extends St.BoxLayout {
    _init(lookingGlass) {
        super._init();

        this._lookingGlass = lookingGlass;
        this._actorData = new Map();
    }

    _showActorChildren(actor) {
        const data = this._actorData.get(actor);
        if (!data || data.visible)
            return;

        data.visible = true;
        data.actorAddedId = actor.connect('child-added', (container, child) => {
            this._addActor(data.children, child);
        });
        data.actorRemovedId = actor.connect('child-removed', (container, child) => {
            this._removeActor(child);
        });

        for (let child of actor)
            this._addActor(data.children, child);
    }

    _hideActorChildren(actor) {
        const data = this._actorData.get(actor);
        if (!data || !data.visible)
            return;

        for (let child of actor)
            this._removeActor(child);

        data.visible = false;
        if (data.actorAddedId > 0) {
            actor.disconnect(data.actorAddedId);
            data.actorAddedId = 0;
        }
        if (data.actorRemovedId > 0) {
            actor.disconnect(data.actorRemovedId);
            data.actorRemovedId = 0;
        }
        data.children.remove_all_children();
    }

    _addActor(container, actor) {
        if (this._actorData.has(actor))
            return;

        if (actor === this._lookingGlass)
            return;

        const button = new ActorLink(actor);
        button.connect('notify::checked', () => {
            this._lookingGlass.setBorderPaintTarget(actor);
            if (button.checked)
                this._showActorChildren(actor);
            else
                this._hideActorChildren(actor);
        });
        button.connect('inspect-actor', () => {
            this._lookingGlass.inspectObject(actor, button);
        });

        const mainContainer = new St.BoxLayout({vertical: true});
        const childrenContainer = new St.BoxLayout({
            vertical: true,
            style: 'padding: 0 0 0 18px',
        });

        mainContainer.add_child(button);
        mainContainer.add_child(childrenContainer);

        this._actorData.set(actor, {
            button,
            container: mainContainer,
            children: childrenContainer,
            visible: false,
            actorAddedId: 0,
            actorRemovedId: 0,
            actorDestroyedId: actor.connect('destroy', () => this._removeActor(actor)),
        });

        let belowChild = null;
        const nextSibling = actor.get_next_sibling();
        if (nextSibling && this._actorData.has(nextSibling))
            belowChild = this._actorData.get(nextSibling).container;

        container.insert_child_above(mainContainer, belowChild);
    }

    _removeActor(actor) {
        const data = this._actorData.get(actor);
        if (!data)
            return;

        for (let child of actor)
            this._removeActor(child);

        if (data.actorAddedId > 0) {
            actor.disconnect(data.actorAddedId);
            data.actorAddedId = 0;
        }
        if (data.actorRemovedId > 0) {
            actor.disconnect(data.actorRemovedId);
            data.actorRemovedId = 0;
        }
        if (data.actorDestroyedId > 0) {
            actor.disconnect(data.actorDestroyedId);
            data.actorDestroyedId = 0;
        }
        data.container.destroy();
        this._actorData.delete(actor);
    }

    vfunc_map() {
        super.vfunc_map();
        this._addActor(this, global.stage);
    }

    vfunc_unmap() {
        super.vfunc_unmap();
        this._removeActor(global.stage);
    }
});

const DebugFlag = GObject.registerClass({
    GTypeFlags: GObject.TypeFlags.ABSTRACT,
}, class DebugFlag extends St.Button {
    _init(label) {
        const box = new St.BoxLayout({x_expand: true});

        const flagLabel = new St.Label({
            text: label,
            x_expand: true,
            x_align: Clutter.ActorAlign.START,
            y_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(flagLabel);

        this._flagSwitch = new PopupMenu.Switch(false);
        this._stateHandler = this._flagSwitch.connect('notify::state', () => {
            if (this._flagSwitch.state)
                this._enable();
            else
                this._disable();
        });

        // Update state whenever the switch is mapped, because most debug flags
        // don't have a way of notifying us of changes.
        this._flagSwitch.connect('notify::mapped', () => {
            if (!this._flagSwitch.is_mapped())
                return;

            const state = this._isEnabled();
            if (state === this._flagSwitch.state)
                return;

            this._flagSwitch.block_signal_handler(this._stateHandler);
            this._flagSwitch.state = state;
            this._flagSwitch.unblock_signal_handler(this._stateHandler);
        });

        box.add_child(this._flagSwitch);

        super._init({
            style_class: 'lg-debug-flag-button',
            can_focus: true,
            toggleMode: true,
            child: box,
            label_actor: flagLabel,
            y_align: Clutter.ActorAlign.CENTER,
        });

        this.connect('clicked', () => this._flagSwitch.toggle());
    }

    _isEnabled() {
        throw new Error('Method not implemented');
    }

    _enable() {
        throw new Error('Method not implemented');
    }

    _disable() {
        throw new Error('Method not implemented');
    }
});


const ClutterDebugFlag = GObject.registerClass(
class ClutterDebugFlag extends DebugFlag {
    _init(categoryName, flagName) {
        super._init(flagName);

        this._argPos = CLUTTER_DEBUG_FLAG_CATEGORIES.get(categoryName).argPos;
        this._enumValue = Clutter[categoryName][flagName];
    }

    _isEnabled() {
        const enabledFlags = Meta.get_clutter_debug_flags();
        return !!(enabledFlags[this._argPos] & this._enumValue);
    }

    _getArgs() {
        const args = [0, 0, 0];
        args[this._argPos] = this._enumValue;
        return args;
    }

    _enable() {
        Meta.add_clutter_debug_flags(...this._getArgs());
    }

    _disable() {
        Meta.remove_clutter_debug_flags(...this._getArgs());
    }
});

const MutterPaintDebugFlag = GObject.registerClass(
class MutterPaintDebugFlag extends DebugFlag {
    _init(flagName) {
        super._init(flagName);

        this._enumValue = Meta.DebugPaintFlag[flagName];
    }

    _isEnabled() {
        return !!(Meta.get_debug_paint_flags() & this._enumValue);
    }

    _enable() {
        Meta.add_debug_paint_flag(this._enumValue);
    }

    _disable() {
        Meta.remove_debug_paint_flag(this._enumValue);
    }
});

const MutterTopicDebugFlag = GObject.registerClass(
class MutterTopicDebugFlag extends DebugFlag {
    _init(flagName) {
        super._init(flagName);

        this._enumValue = Meta.DebugTopic[flagName];
    }

    _isEnabled() {
        return Meta.is_topic_enabled(this._enumValue);
    }

    _enable() {
        Meta.add_verbose_topic(this._enumValue);
    }

    _disable() {
        Meta.remove_verbose_topic(this._enumValue);
    }
});

const UnsafeModeDebugFlag = GObject.registerClass(
class UnsafeModeDebugFlag extends DebugFlag {
    _init() {
        super._init('unsafe-mode');
    }

    _isEnabled() {
        return global.context.unsafe_mode;
    }

    _enable() {
        global.context.unsafe_mode = true;
    }

    _disable() {
        global.context.unsafe_mode = false;
    }
});

const DebugFlags = GObject.registerClass(
class DebugFlags extends St.BoxLayout {
    _init() {
        super._init({
            name: 'lookingGlassDebugFlags',
            vertical: true,
            x_align: Clutter.ActorAlign.CENTER,
        });

        // Clutter debug flags
        for (const [categoryName, props] of CLUTTER_DEBUG_FLAG_CATEGORIES.entries()) {
            this._addHeader(`Clutter${categoryName}`);
            for (const flagName of this._getFlagNames(Clutter[categoryName])) {
                if (props.exclude.includes(flagName))
                    continue;
                this.add_child(new ClutterDebugFlag(categoryName, flagName));
            }
        }

        // Meta paint flags
        this._addHeader('MetaDebugPaintFlag');
        for (const flagName of this._getFlagNames(Meta.DebugPaintFlag))
            this.add_child(new MutterPaintDebugFlag(flagName));

        // Meta debug topics
        this._addHeader('MetaDebugTopic');
        for (const flagName of this._getFlagNames(Meta.DebugTopic))
            this.add_child(new MutterTopicDebugFlag(flagName));

        // MetaContext::unsafe-mode
        this._addHeader('MetaContext');
        this.add_child(new UnsafeModeDebugFlag());
    }

    _addHeader(title) {
        const header = new St.Label({
            text: title,
            style_class: 'lg-debug-flags-header',
            x_align: Clutter.ActorAlign.START,
        });
        this.add_child(header);
    }

    *_getFlagNames(enumObject) {
        for (const flagName of Object.getOwnPropertyNames(enumObject)) {
            if (typeof enumObject[flagName] !== 'number')
                continue;

            if (enumObject[flagName] <= 0)
                continue;

            yield flagName;
        }
    }
});


export const LookingGlass = GObject.registerClass(
class LookingGlass extends St.BoxLayout {
    _init() {
        super._init({
            name: 'LookingGlassDialog',
            style_class: 'lg-dialog',
            vertical: true,
            visible: false,
            reactive: true,
        });

        this._borderPaintTarget = null;
        this._redBorderEffect = new RedBorderEffect();

        this._open = false;

        this._it = null;
        this._offset = 0;

        // Sort of magic, but...eh.
        this._maxItems = 150;

        this._interfaceSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.interface'});
        this._interfaceSettings.connect('changed::monospace-font-name',
            this._updateFont.bind(this));
        this._updateFont();

        // We want it to appear to slide out from underneath the panel
        Main.uiGroup.add_child(this);
        Main.uiGroup.set_child_below_sibling(this,
            Main.layoutManager.panelBox);
        Main.layoutManager.panelBox.connect('notify::allocation',
            this._queueResize.bind(this));
        Main.layoutManager.keyboardBox.connect('notify::allocation',
            this._queueResize.bind(this));

        this._objInspector = new ObjInspector(this);
        Main.uiGroup.add_child(this._objInspector);
        this._objInspector.hide();

        let toolbar = new St.BoxLayout({name: 'Toolbar'});
        this.add_child(toolbar);
        const inspectButton = new St.Button({
            style_class: 'lg-toolbar-button',
            icon_name: 'find-location-symbolic',
        });
        toolbar.add_child(inspectButton);
        inspectButton.connect('clicked', () => {
            let inspector = new Inspector(this);
            inspector.connect('target', (i, target, stageX, stageY) => {
                this._pushResult(`inspect(${Math.round(stageX)}, ${Math.round(stageY)})`, target);
            });
            inspector.connect('closed', () => {
                this.show();
                global.stage.set_key_focus(this._entry);
            });
            this.hide();
            return Clutter.EVENT_STOP;
        });

        const gcButton = new St.Button({
            style_class: 'lg-toolbar-button',
            icon_name: 'user-trash-full-symbolic',
        });
        toolbar.add_child(gcButton);
        gcButton.connect('clicked', () => {
            gcButton.child.icon_name = 'user-trash-symbolic';
            System.gc();
            this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
                gcButton.child.icon_name = 'user-trash-full-symbolic';
                this._timeoutId = 0;
                return GLib.SOURCE_REMOVE;
            });
            GLib.Source.set_name_by_id(
                this._timeoutId,
                '[gnome-shell] gcButton.child.icon_name = \'user-trash-full-symbolic\''
            );
            return Clutter.EVENT_PROPAGATE;
        });

        let notebook = new Notebook();
        this._notebook = notebook;
        this.add_child(notebook);

        let emptyBox = new St.Bin({x_expand: true});
        toolbar.add_child(emptyBox);
        toolbar.add_child(notebook.tabControls);

        this._evalBox = new St.BoxLayout({name: 'EvalBox', vertical: true});
        notebook.appendPage('Evaluator', this._evalBox);

        this._resultsArea = new St.BoxLayout({
            name: 'ResultsArea',
            vertical: true,
            y_expand: true,
        });
        this._evalBox.add_child(this._resultsArea);

        this._entryArea = new St.BoxLayout({
            name: 'EntryArea',
            y_align: Clutter.ActorAlign.END,
        });
        this._evalBox.add_child(this._entryArea);

        let label = new St.Label({text: CHEVRON});
        this._entryArea.add_child(label);

        this._entry = new St.Entry({
            can_focus: true,
            x_expand: true,
        });
        ShellEntry.addContextMenu(this._entry);
        this._entryArea.add_child(this._entry);

        this._windowList = new WindowList(this);
        notebook.appendPage('Windows', this._windowList);

        this._extensions = new Extensions(this);
        notebook.appendPage('Extensions', this._extensions);

        this._actorTreeViewer = new ActorTreeViewer(this);
        notebook.appendPage('Actors', this._actorTreeViewer);

        this._debugFlags = new DebugFlags();
        notebook.appendPage('Flags', this._debugFlags);

        this._entry.clutter_text.connect('activate', (o, _e) => {
            // Hide any completions we are currently showing
            this._hideCompletions();

            let text = o.get_text();
            // Ensure we don't get newlines in the command; the history file is
            // newline-separated.
            text = text.replace('\n', ' ');
            this._evaluate(text).catch(logError);
            return true;
        });

        this._history = new History.HistoryManager({
            gsettingsKey: HISTORY_KEY,
            entry: this._entry.clutter_text,
        });

        this._autoComplete = new AutoComplete(this._entry);
        this._autoComplete.connect('suggest', (a, e) => {
            this._showCompletions(e.completions);
        });
        // If a completion is completed unambiguously, the currently-displayed completion
        // suggestions become irrelevant.
        this._autoComplete.connect('completion', (a, e) => {
            if (e.type === 'whole-word')
                this._hideCompletions();
        });

        this._resize();
    }

    vfunc_captured_event(event) {
        if (Main.keyboard.maybeHandleEvent(event))
            return Clutter.EVENT_STOP;

        return Clutter.EVENT_PROPAGATE;
    }

    _updateFont() {
        let fontName = this._interfaceSettings.get_string('monospace-font-name');
        let fontDesc = Pango.FontDescription.from_string(fontName);
        // We ignore everything but size and style; you'd be crazy to set your system-wide
        // monospace font to be bold/oblique/etc. Could easily be added here.
        let size = fontDesc.get_size() / 1024.;
        let unit = fontDesc.get_size_is_absolute() ? 'px' : 'pt';
        this.style = `
            font-size: ${size}${unit};
            font-family: "${fontDesc.get_family()}";`;
    }

    setBorderPaintTarget(obj) {
        if (this._borderPaintTarget != null)
            this._borderPaintTarget.remove_effect(this._redBorderEffect);
        this._borderPaintTarget = obj;
        if (this._borderPaintTarget != null)
            this._borderPaintTarget.add_effect(this._redBorderEffect);
    }

    _pushResult(command, obj) {
        let index = this._resultsArea.get_n_children() + this._offset;
        let result = new Result(this, CHEVRON + command, obj, index);
        this._resultsArea.add_child(result);
        if (obj instanceof Clutter.Actor)
            this.setBorderPaintTarget(obj);

        if (this._resultsArea.get_n_children() > this._maxItems) {
            this._resultsArea.get_first_child().destroy();
            this._offset++;
        }
        this._it = obj;

        // Scroll to bottom
        this._notebook.scrollToBottom(0);
    }

    _showCompletions(completions) {
        if (!this._completionActor) {
            this._completionActor = new St.Label({name: 'LookingGlassAutoCompletionText', style_class: 'lg-completions-text'});
            this._completionActor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
            this._completionActor.clutter_text.line_wrap = true;
            this._evalBox.insert_child_below(this._completionActor, this._entryArea);
        }

        this._completionActor.set_text(completions.join(', '));

        // Setting the height to -1 allows us to get its actual preferred height rather than
        // whatever was last set when animating
        this._completionActor.set_height(-1);
        let [, naturalHeight] = this._completionActor.get_preferred_height(this._resultsArea.get_width());

        // Don't reanimate if we are already visible
        if (this._completionActor.visible) {
            this._completionActor.height = naturalHeight;
        } else {
            let settings = St.Settings.get();
            let duration = AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION / settings.slow_down_factor;
            this._completionActor.show();
            this._completionActor.remove_all_transitions();
            this._completionActor.ease({
                height: naturalHeight,
                opacity: 255,
                duration,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        }
    }

    _hideCompletions() {
        if (this._completionActor) {
            let settings = St.Settings.get();
            let duration = AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION / settings.slow_down_factor;
            this._completionActor.remove_all_transitions();
            this._completionActor.ease({
                height: 0,
                opacity: 0,
                duration,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this._completionActor.hide();
                },
            });
        }
    }

    async _evaluate(command) {
        command = this._history.addItem(command); // trims command
        if (!command)
            return;

        let lines = command.split(';');
        lines.push(`return ${lines.pop()}`);

        let fullCmd = commandHeader + lines.join(';');

        let resultObj;
        try {
            resultObj = await AsyncFunction(fullCmd)();
        } catch (e) {
            resultObj = `<exception ${e}>`;
        }

        this._pushResult(command, resultObj);
        this._entry.text = '';
    }

    inspect(x, y) {
        return global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);
    }

    getIt() {
        return this._it;
    }

    getResult(idx) {
        try {
            return this._resultsArea.get_child_at_index(idx - this._offset).o;
        } catch (e) {
            throw new Error(`Unknown result at index ${idx}`);
        }
    }

    toggle() {
        if (this._open)
            this.close();
        else
            this.open();
    }

    _queueResize() {
        const laters = global.compositor.get_laters();
        laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
            this._resize();
            return GLib.SOURCE_REMOVE;
        });
    }

    _resize() {
        let primary = Main.layoutManager.primaryMonitor;
        let myWidth = primary.width * 0.7;
        let availableHeight = primary.height - Main.layoutManager.keyboardBox.height;
        let myHeight = Math.min(primary.height * 0.7, availableHeight * 0.9);
        this.x = primary.x + (primary.width - myWidth) / 2;
        this._hiddenY = primary.y + Main.layoutManager.panelBox.height - myHeight;
        this._targetY = this._hiddenY + myHeight;
        this.y = this._hiddenY;
        this.width = myWidth;
        this.height = myHeight;
        this._objInspector.set_size(Math.floor(myWidth * 0.8), Math.floor(myHeight * 0.8));
        this._objInspector.set_position(
            this.x + Math.floor(myWidth * 0.1),
            this._targetY + Math.floor(myHeight * 0.1));
    }

    insertObject(obj) {
        this._pushResult('<insert>', obj);
    }

    inspectObject(obj, sourceActor) {
        this._objInspector.open(sourceActor);
        this._objInspector.selectObject(obj);
    }

    // Handle key events which are relevant for all tabs of the LookingGlass
    vfunc_key_press_event(event) {
        let symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_Escape) {
            this.close();
            return Clutter.EVENT_STOP;
        }
        // Ctrl+PgUp and Ctrl+PgDown switches tabs in the notebook view
        if (event.get_state() & Clutter.ModifierType.CONTROL_MASK) {
            if (symbol === Clutter.KEY_Page_Up)
                this._notebook.prevTab();
            else if (symbol === Clutter.KEY_Page_Down)
                this._notebook.nextTab();
        }
        return super.vfunc_key_press_event(event);
    }

    open() {
        if (this._open)
            return;

        let grab = Main.pushModal(this, {actionMode: Shell.ActionMode.LOOKING_GLASS});
        if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
            Main.popModal(grab);
            return;
        }

        this._grab = grab;
        this._notebook.selectIndex(0);
        this.show();
        this._open = true;
        this._history.lastItem();

        this.remove_all_transitions();

        // We inverse compensate for the slow-down so you can change the factor
        // through LookingGlass without long waits.
        let duration = LG_ANIMATION_TIME / St.Settings.get().slow_down_factor;
        this.ease({
            y: this._targetY,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        this._windowList.update();
        this._entry.grab_key_focus();
    }

    close() {
        if (!this._open)
            return;

        this._objInspector.hide();

        this._open = false;
        this.remove_all_transitions();

        this.setBorderPaintTarget(null);

        let settings = St.Settings.get();
        let duration = Math.min(
            LG_ANIMATION_TIME / settings.slow_down_factor,
            LG_ANIMATION_TIME);
        this.ease({
            y: this._hiddenY,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                Main.popModal(this._grab);
                this._grab = null;
                this.hide();
            },
        });
    }

    get isOpen() {
        return this._open;
    }
});
(uuay)edgeDragAction.jsd// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Mtk from 'gi://Mtk';
import St from 'gi://St';

import * as Main from './main.js';

const EDGE_THRESHOLD = 20;
const DRAG_DISTANCE = 80;

export const EdgeDragAction = GObject.registerClass({
    Signals: {
        'activated': {},
        'progress': {param_types: [GObject.TYPE_DOUBLE]},
    },
}, class EdgeDragAction extends Clutter.GestureAction {
    _init(side, allowedModes) {
        super._init();
        this._side = side;
        this._allowedModes = allowedModes;
        this.set_n_touch_points(1);
        this.set_threshold_trigger_edge(Clutter.GestureTriggerEdge.AFTER);
    }

    _getMonitorRect(x, y) {
        const rect = new Mtk.Rectangle({x: x - 1, y: y - 1, width: 1, height: 1});
        let monitorIndex = global.display.get_monitor_index_for_rect(rect);

        return global.display.get_monitor_geometry(monitorIndex);
    }

    vfunc_gesture_prepare(_actor) {
        if (this.get_n_current_points() === 0)
            return false;

        if (!(this._allowedModes & Main.actionMode))
            return false;

        let [x, y] = this.get_press_coords(0);
        let monitorRect = this._getMonitorRect(x, y);

        return (this._side === St.Side.LEFT && x < monitorRect.x + EDGE_THRESHOLD) ||
                (this._side === St.Side.RIGHT && x > monitorRect.x + monitorRect.width - EDGE_THRESHOLD) ||
                (this._side === St.Side.TOP && y < monitorRect.y + EDGE_THRESHOLD) ||
                (this._side === St.Side.BOTTOM && y > monitorRect.y + monitorRect.height - EDGE_THRESHOLD);
    }

    vfunc_gesture_progress(_actor) {
        let [startX, startY] = this.get_press_coords(0);
        let [x, y] = this.get_motion_coords(0);
        let offsetX = Math.abs(x - startX);
        let offsetY = Math.abs(y - startY);

        if (offsetX < EDGE_THRESHOLD && offsetY < EDGE_THRESHOLD)
            return true;

        if ((offsetX > offsetY &&
             (this._side === St.Side.TOP || this._side === St.Side.BOTTOM)) ||
            (offsetY > offsetX &&
             (this._side === St.Side.LEFT || this._side === St.Side.RIGHT))) {
            this.cancel();
            return false;
        }

        if (this._side === St.Side.TOP ||
            this._side === St.Side.BOTTOM)
            this.emit('progress', offsetY);
        else
            this.emit('progress', offsetX);

        return true;
    }

    vfunc_gesture_end(_actor) {
        let [startX, startY] = this.get_press_coords(0);
        let [x, y] = this.get_motion_coords(0);
        let monitorRect = this._getMonitorRect(startX, startY);

        if ((this._side === St.Side.TOP && y > monitorRect.y + DRAG_DISTANCE) ||
            (this._side === St.Side.BOTTOM && y < monitorRect.y + monitorRect.height - DRAG_DISTANCE) ||
            (this._side === St.Side.LEFT && x > monitorRect.x + DRAG_DISTANCE) ||
            (this._side === St.Side.RIGHT && x < monitorRect.x + monitorRect.width - DRAG_DISTANCE))
            this.emit('activated');
        else
            this.cancel();
    }
});
(uuay)jsParse.js�"/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */

const AsyncFunction = async function () {}.constructor;

/**
 * Returns a list of potential completions for text. Completions either
 * follow a dot (e.g. foo.ba -> bar) or they are picked from globalCompletionList (e.g. fo -> foo)
 * commandHeader is prefixed on any expression before it is eval'ed.  It will most likely
 * consist of global constants that might not carry over from the calling environment.
 *
 * This function is likely the one you want to call from external modules
 *
 * @param {string} text
 * @param {string} commandHeader
 * @param {readonly string[]} [globalCompletionList]
 */
export async function getCompletions(text, commandHeader, globalCompletionList) {
    let methods = [];
    let expr_, base;
    let attrHead = '';
    if (globalCompletionList == null)
        globalCompletionList = [];

    let offset = getExpressionOffset(text, text.length - 1);
    if (offset >= 0) {
        text = text.slice(offset);

        // Look for expressions like "Main.panel.foo" and match Main.panel and foo
        let matches = text.match(/(.*)\.(.*)/);
        if (matches) {
            [expr_, base, attrHead] = matches;

            methods = (await getPropertyNamesFromExpression(base, commandHeader)).filter(
                attr => attr.slice(0, attrHead.length) === attrHead);
        }

        // Look for the empty expression or partially entered words
        // not proceeded by a dot and match them against global constants
        matches = text.match(/^(\w*)$/);
        if (text === '' || matches) {
            [expr_, attrHead] = matches;
            methods = globalCompletionList.filter(
                attr => attr.slice(0, attrHead.length) === attrHead);
        }
    }

    return [methods, attrHead];
}


/**
 * A few functions for parsing strings of javascript code.
 */

/**
 * Identify characters that delimit an expression.  That is,
 * if we encounter anything that isn't a letter, '.', ')', or ']',
 * we should stop parsing.
 *
 * @param {string} c
 */
function isStopChar(c) {
    return !c.match(/[\w.)\]]/);
}

/**
 * Given the ending position of a quoted string, find where it starts
 *
 * @param {string} expr
 * @param {number} offset
 */
export function findMatchingQuote(expr, offset) {
    let quoteChar = expr.charAt(offset);
    for (let i = offset - 1; i >= 0; --i) {
        if (expr.charAt(i) === quoteChar && expr.charAt(i - 1) !== '\\')
            return i;
    }
    return -1;
}

/**
 * Given the ending position of a regex, find where it starts
 *
 * @param {string} expr
 * @param {number} offset
 */
export function findMatchingSlash(expr, offset) {
    for (let i = offset - 1; i >= 0; --i) {
        if (expr.charAt(i) === '/' && expr.charAt(i - 1) !== '\\')
            return i;
    }
    return -1;
}

/**
 * If expr.charAt(offset) is ')' or ']',
 * return the position of the corresponding '(' or '[' bracket.
 * This function does not check for syntactic correctness.  e.g.,
 * findMatchingBrace("[(])", 3) returns 1.
 *
 * @param {string} expr
 * @param {number} offset
 */
export function findMatchingBrace(expr, offset) {
    let closeBrace = expr.charAt(offset);
    let openBrace = {')': '(', ']': '['}[closeBrace];

    return findTheBrace(expr, offset - 1, openBrace, closeBrace);
}

/**
 * @param {*} expr
 * @param {*} offset
 * @param  {...any} braces
 * @returns {number}
 */
export function findTheBrace(expr, offset, ...braces) {
    let [openBrace, closeBrace] = braces;

    if (offset < 0)
        return -1;

    if (expr.charAt(offset) === openBrace)
        return offset;

    if (expr.charAt(offset).match(/['"]/))
        return findTheBrace(expr, findMatchingQuote(expr, offset) - 1, ...braces);

    if (expr.charAt(offset) === '/')
        return findTheBrace(expr, findMatchingSlash(expr, offset) - 1, ...braces);

    if (expr.charAt(offset) === closeBrace)
        return findTheBrace(expr, findTheBrace(expr, offset - 1, ...braces) - 1, ...braces);

    return findTheBrace(expr, offset - 1, ...braces);
}

/**
 * Walk expr backwards from offset looking for the beginning of an
 * expression suitable for passing to eval.
 * There is no guarantee of correct javascript syntax between the return
 * value and offset.  This function is meant to take a string like
 * "foo(Obj.We.Are.Completing" and allow you to extract "Obj.We.Are.Completing"
 *
 * @param {string} expr
 * @param {number} offset
 */
export function getExpressionOffset(expr, offset) {
    while (offset >= 0) {
        let currChar = expr.charAt(offset);

        if (isStopChar(currChar))
            return offset + 1;

        if (currChar.match(/[)\]]/))
            offset = findMatchingBrace(expr, offset);

        --offset;
    }

    return offset + 1;
}

/**
 * Things with non-word characters or that start with a number
 * are not accessible via .foo notation and so aren't returned
 *
 * @param {string} w
 */
function isValidPropertyName(w) {
    return !(w.match(/\W/) || w.match(/^\d/));
}

/**
 * To get all properties (enumerable and not), we need to walk
 * the prototype chain ourselves
 *
 * @param {object} obj
 */
export function getAllProps(obj) {
    if (obj === null || obj === undefined)
        return [];

    return Object.getOwnPropertyNames(obj).concat(getAllProps(Object.getPrototypeOf(obj)));
}

/**
 * Given a string _expr_, returns all methods
 * that can be accessed via '.' notation.
 * e.g., expr="({ foo: null, bar: null, 4: null })" will
 * return ["foo", "bar", ...] but the list will not include "4",
 * since methods accessed with '.' notation must star with a letter or _.
 *
 * @param {string} expr
 * @param {string=} commandHeader
 */
export async function getPropertyNamesFromExpression(expr, commandHeader = '') {
    let obj = {};
    if (!isUnsafeExpression(expr)) {
        try {
            const lines = expr.split('\n');
            lines.push(`return ${lines.pop()}`);
            obj = await AsyncFunction(commandHeader + lines.join(';'))();
        } catch (e) {
            return [];
        }
    } else {
        return [];
    }

    let propsUnique = {};
    if (typeof obj === 'object') {
        let allProps = getAllProps(obj);
        // Get only things we are allowed to complete following a '.'
        allProps = allProps.filter(isValidPropertyName);

        // Make sure propsUnique contains one key for every
        // property so we end up with a unique list of properties
        allProps.map(p => (propsUnique[p] = null));
    }
    return Object.keys(propsUnique).sort();
}

/**
 * Given a list of words, returns the longest prefix they all have in common
 *
 * @param {readonly string[]} words
 */
export function getCommonPrefix(words) {
    let word = words[0];
    for (let i = 0; i < word.length; i++) {
        for (let w = 1; w < words.length; w++) {
            if (words[w].charAt(i) !== word.charAt(i))
                return word.slice(0, i);
        }
    }
    return word;
}

/**
 * Remove any blocks that are quoted or are in a regex
 *
 * @param {string} str
 */
export function removeLiterals(str) {
    if (str.length === 0)
        return '';

    let currChar = str.charAt(str.length - 1);
    if (currChar === '"' || currChar === '\'') {
        return removeLiterals(
            str.slice(0, findMatchingQuote(str, str.length - 1)));
    } else if (currChar === '/') {
        return removeLiterals(
            str.slice(0, findMatchingSlash(str, str.length - 1)));
    }

    return removeLiterals(str.slice(0, str.length - 1)) + currChar;
}

/**
 * Returns true if there is reason to think that eval(str)
 * will modify the global scope
 *
 * @param {string} str
 */
export function isUnsafeExpression(str) {
    // Check for any sort of assignment
    // The strategy used is dumb: remove any quotes
    // or regexs and comparison operators and see if there is an '=' character.
    // If there is, it might be an unsafe assignment.

    let prunedStr = removeLiterals(str);
    prunedStr = prunedStr.replace(/[=!]==/g, '');    // replace === and !== with nothing
    prunedStr = prunedStr.replace(/[=<>!]=/g, '');    // replace ==, <=, >=, != with nothing

    if (prunedStr.match(/[=]/)) {
        return true;
    } else if (prunedStr.match(/;/)) {
        // If we contain a semicolon not inside of a quote/regex, assume we're unsafe as well
        return true;
    }

    return false;
}

/**
 * Returns a list of global keywords derived from str
 *
 * @param {string} str
 */
export function getDeclaredConstants(str) {
    let ret = [];
    str.split(';').forEach(s => {
        let base_, keyword;
        let match = s.match(/const\s+(\w+)\s*=/);
        if (match) {
            [base_, keyword] = match;
            ret.push(keyword);
        }
    });

    return ret;
}
(uuay)workspaceSwitcherPopup.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St';

import * as Layout from './layout.js';
import * as Main from './main.js';

const ANIMATION_TIME = 100;
const DISPLAY_TIMEOUT = 600;


export const WorkspaceSwitcherPopup = GObject.registerClass(
class WorkspaceSwitcherPopup extends Clutter.Actor {
    _init() {
        super._init({
            offscreen_redirect: Clutter.OffscreenRedirect.ALWAYS,
            x_expand: true,
            y_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.END,
        });

        const constraint = new Layout.MonitorConstraint({primary: true});
        this.add_constraint(constraint);

        Main.uiGroup.add_child(this);

        this._timeoutId = 0;

        this._list = new St.BoxLayout({
            style_class: 'workspace-switcher',
        });
        this.add_child(this._list);

        this._redisplay();

        this.hide();

        let workspaceManager = global.workspace_manager;
        workspaceManager.connectObject(
            'workspace-added', this._redisplay.bind(this),
            'workspace-removed', this._redisplay.bind(this), this);

        this.connect('destroy', this._onDestroy.bind(this));
    }

    _redisplay() {
        let workspaceManager = global.workspace_manager;

        this._list.destroy_all_children();

        for (let i = 0; i < workspaceManager.n_workspaces; i++) {
            const indicator = new St.Bin({
                style_class: 'ws-switcher-indicator',
            });

            if (i === this._activeWorkspaceIndex)
                indicator.add_style_pseudo_class('active');

            this._list.add_child(indicator);
        }
    }

    display(activeWorkspaceIndex) {
        this._activeWorkspaceIndex = activeWorkspaceIndex;

        this._redisplay();
        if (this._timeoutId !== 0)
            GLib.source_remove(this._timeoutId);
        this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DISPLAY_TIMEOUT, this._onTimeout.bind(this));
        GLib.Source.set_name_by_id(this._timeoutId, '[gnome-shell] this._onTimeout');

        const duration = this.visible ? 0 : ANIMATION_TIME;
        this.show();
        this.opacity = 0;
        this.ease({
            opacity: 255,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    _onTimeout() {
        GLib.source_remove(this._timeoutId);
        this._timeoutId = 0;
        this.ease({
            opacity: 0.0,
            duration: ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this.destroy(),
        });
        return GLib.SOURCE_REMOVE;
    }

    _onDestroy() {
        if (this._timeoutId)
            GLib.source_remove(this._timeoutId);
        this._timeoutId = 0;
    }
});
(uuay)desktop.jsm// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GLib from 'gi://GLib';

// current desktop doesn't change unless we restart the shell or control
// the env variable. It's safe to cache matching result
const _currentDesktopsMatches = new Map();

// is:
// @name: desktop string you want to assert if it matches the current desktop env
//
// The function examples XDG_CURRENT_DESKTOP and return if the current desktop
// is part of that desktop string.
//
// Return value: if the environment isn't set or doesn't match, return False
// otherwise, return True.
export function is(name) {
    if (!_currentDesktopsMatches.size) {
        const desktopsEnv = GLib.getenv('XDG_CURRENT_DESKTOP');
        if (!desktopsEnv) {
            _currentDesktopsMatches.set(name, false);
            return false;
        }

        const desktops = desktopsEnv.split(':');
        desktops.forEach(d => _currentDesktopsMatches.set(d, true));

        if (!_currentDesktopsMatches.size)
            _currentDesktopsMatches.set(name, _currentDesktopsMatches.has(name));
    }

    return !!_currentDesktopsMatches.get(name);
}
(uuay)keyboard.jsw// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Graphene from 'gi://Graphene';
import IBus from 'gi://IBus';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';

import * as EdgeDragAction from './edgeDragAction.js';
import * as InputSourceManager from './status/keyboard.js';
import * as IBusManager from '../misc/ibusManager.js';
import * as BoxPointer from './boxpointer.js';
import * as Main from './main.js';
import * as PageIndicators from './pageIndicators.js';
import * as PopupMenu from './popupMenu.js';
import * as SwipeTracker from './swipeTracker.js';

const KEYBOARD_ANIMATION_TIME = 150;
const KEYBOARD_REST_TIME = KEYBOARD_ANIMATION_TIME * 2;
const KEY_LONG_PRESS_TIME = 250;

const A11Y_APPLICATIONS_SCHEMA = 'org.gnome.desktop.a11y.applications';
const SHOW_KEYBOARD = 'screen-keyboard-enabled';
const EMOJI_PAGE_SEPARATION = 32;

/* KeyContainer puts keys in a grid where a 1:1 key takes this size */
const KEY_SIZE = 2;

const KEY_RELEASE_TIMEOUT = 50;
const BACKSPACE_WORD_DELETE_THRESHOLD = 50;

const AspectContainer = GObject.registerClass(
class AspectContainer extends St.Widget {
    _init(params) {
        super._init(params);
        this._ratio = 1;
    }

    setRatio(relWidth, relHeight) {
        this._ratio = relWidth / relHeight;
        this.queue_relayout();
    }

    vfunc_get_preferred_width(forHeight) {
        let [min, nat] = super.vfunc_get_preferred_width(forHeight);

        if (forHeight > 0)
            nat = forHeight * this._ratio;

        return [min, nat];
    }

    vfunc_get_preferred_height(forWidth) {
        let [min, nat] = super.vfunc_get_preferred_height(forWidth);

        if (forWidth > 0)
            nat = forWidth / this._ratio;

        return [min, nat];
    }

    vfunc_allocate(box) {
        if (box.get_width() > 0 && box.get_height() > 0) {
            let sizeRatio = box.get_width() / box.get_height();
            if (sizeRatio >= this._ratio) {
                /* Restrict horizontally */
                let width = box.get_height() * this._ratio;
                let diff = box.get_width() - width;

                box.x1 += Math.floor(diff / 2);
                box.x2 -= Math.ceil(diff / 2);
            }
        }

        super.vfunc_allocate(box);
    }
});

const KeyContainer = GObject.registerClass(
class KeyContainer extends St.Widget {
    _init() {
        const gridLayout = new Clutter.GridLayout({
            orientation: Clutter.Orientation.HORIZONTAL,
            column_homogeneous: true,
            row_homogeneous: true,
        });
        super._init({
            layout_manager: gridLayout,
            x_expand: true,
            y_expand: true,
        });
        this._gridLayout = gridLayout;
        this._nRows = 0;
        this._currentCol = 0;
        this._maxCols = 0;
    }

    appendRow() {
        this._nRows++;
        this._currentCol = 0;
    }

    appendKey(key, width = 1, height = 1, leftOffset = 0) {
        const left = this._currentCol + leftOffset;
        const top = this._nRows;
        this._gridLayout.attach(key,
            left * KEY_SIZE, top * KEY_SIZE,
            width * KEY_SIZE, height * KEY_SIZE);

        this._currentCol += leftOffset + width;
        this._maxCols = Math.max(this._currentCol, this._maxCols);
    }

    getRatio() {
        return [this._maxCols, this._nRows];
    }
});

const Suggestions = GObject.registerClass(
class Suggestions extends St.BoxLayout {
    _init() {
        super._init({
            style_class: 'word-suggestions',
            vertical: false,
            x_align: Clutter.ActorAlign.CENTER,
        });
        this.show();
    }

    add(word, callback) {
        let button = new St.Button({label: word});
        button.connect('button-press-event', () => {
            callback();
            return Clutter.EVENT_STOP;
        });
        button.connect('touch-event', (actor, event) => {
            if (event.type() !== Clutter.EventType.TOUCH_BEGIN)
                return Clutter.EVENT_PROPAGATE;

            callback();
            return Clutter.EVENT_STOP;
        });
        this.add_child(button);
    }

    clear() {
        this.remove_all_children();
    }

    setVisible(visible) {
        for (const child of this)
            child.visible = visible;
    }
});

class LanguageSelectionPopup extends PopupMenu.PopupMenu {
    constructor(actor) {
        super(actor, 0.5, St.Side.BOTTOM);

        let inputSourceManager = InputSourceManager.getInputSourceManager();
        let inputSources = inputSourceManager.inputSources;

        let item;
        for (let i in inputSources) {
            let is = inputSources[i];

            item = this.addAction(is.displayName, () => {
                inputSourceManager.activateInputSource(is, true);
            });
            item.can_focus = false;
            item.setOrnament(is === inputSourceManager.currentSource
                ? PopupMenu.Ornament.DOT
                : PopupMenu.Ornament.NO_DOT);
        }

        this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        item = this.addSettingsAction(_('Keyboard Settings'), 'gnome-keyboard-panel.desktop');
        item.can_focus = false;

        actor.connectObject('notify::mapped', () => {
            if (!actor.is_mapped())
                this.close(true);
        }, this);
    }

    _onCapturedEvent(actor, event) {
        const targetActor = global.stage.get_event_actor(event);

        if (targetActor === this.actor ||
            this.actor.contains(targetActor))
            return Clutter.EVENT_PROPAGATE;

        if (event.type() === Clutter.EventType.BUTTON_RELEASE || event.type() === Clutter.EventType.TOUCH_END)
            this.close(true);

        return Clutter.EVENT_STOP;
    }

    open(animate) {
        super.open(animate);
        global.stage.connectObject(
            'captured-event', this._onCapturedEvent.bind(this), this);
    }

    close(animate) {
        super.close(animate);
        global.stage.disconnectObject(this);
    }

    destroy() {
        global.stage.disconnectObject(this);
        this.sourceActor.disconnectObject(this);
        super.destroy();
    }
}

const Key = GObject.registerClass({
    Signals: {
        'long-press': {},
        'pressed': {},
        'released': {},
        'keyval': {param_types: [GObject.TYPE_UINT]},
        'commit': {param_types: [GObject.TYPE_STRING]},
    },
}, class Key extends St.BoxLayout {
    _init(params, extendedKeys = []) {
        const {label, iconName, commitString, keyval} = {keyval: 0, ...params};
        super._init({style_class: 'key-container'});

        this._keyval = parseInt(keyval, 16);
        this.keyButton = this._makeKey(commitString, label, iconName);

        /* Add the key in a container, so keys can be padded without losing
         * logical proportions between those.
         */
        this.add_child(this.keyButton);
        this.connect('destroy', this._onDestroy.bind(this));

        this._extendedKeys = extendedKeys;
        this._extendedKeyboard = null;
        this._pressTimeoutId = 0;
        this._capturedPress = false;
    }

    get iconName() {
        return this._icon.icon_name;
    }

    set iconName(value) {
        this._icon.icon_name = value;
    }

    _onDestroy() {
        if (this._boxPointer) {
            this._boxPointer.destroy();
            this._boxPointer = null;
        }

        this.cancel();
    }

    _ensureExtendedKeysPopup() {
        if (this._extendedKeys.length === 0)
            return;

        if (this._boxPointer)
            return;

        this._boxPointer = new BoxPointer.BoxPointer(St.Side.BOTTOM);
        this._boxPointer.hide();
        Main.layoutManager.addTopChrome(this._boxPointer);
        this._boxPointer.setPosition(this.keyButton, 0.5);

        // Adds style to existing keyboard style to avoid repetition
        this._boxPointer.add_style_class_name('keyboard-subkeys');
        this._getExtendedKeys();
        this.keyButton._extendedKeys = this._extendedKeyboard;
    }

    _press(button) {
        if (button === this.keyButton) {
            this._pressTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
                KEY_LONG_PRESS_TIME,
                () => {
                    this._pressTimeoutId = 0;

                    this.emit('long-press');

                    if (this._extendedKeys.length > 0) {
                        this._touchPressSlot = null;
                        this._ensureExtendedKeysPopup();
                        this.keyButton.set_hover(false);
                        this.keyButton.fake_release();
                        this._showSubkeys();
                    }

                    return GLib.SOURCE_REMOVE;
                });
        }

        this.emit('pressed');
        this._pressed = true;
    }

    _release(button, commitString) {
        if (this._pressTimeoutId !== 0) {
            GLib.source_remove(this._pressTimeoutId);
            this._pressTimeoutId = 0;
        }

        if (this._pressed) {
            if (this._keyval && button === this.keyButton)
                this.emit('keyval', this._keyval);
            else if (commitString)
                this.emit('commit', commitString);
            else
                console.error('Need keyval or commitString');
        }

        this.emit('released');
        this._hideSubkeys();
        this._pressed = false;
    }

    cancel() {
        if (this._pressTimeoutId !== 0) {
            GLib.source_remove(this._pressTimeoutId);
            this._pressTimeoutId = 0;
        }
        this._touchPressSlot = null;
        this.keyButton.set_hover(false);
        this.keyButton.fake_release();
    }

    _onCapturedEvent(actor, event) {
        let type = event.type();
        let press = type === Clutter.EventType.BUTTON_PRESS || type === Clutter.EventType.TOUCH_BEGIN;
        let release = type === Clutter.EventType.BUTTON_RELEASE || type === Clutter.EventType.TOUCH_END;
        const targetActor = global.stage.get_event_actor(event);

        if (targetActor === this._boxPointer.bin ||
            this._boxPointer.bin.contains(targetActor))
            return Clutter.EVENT_PROPAGATE;

        if (press)
            this._capturedPress = true;
        else if (release && this._capturedPress)
            this._hideSubkeys();

        return Clutter.EVENT_STOP;
    }

    _showSubkeys() {
        this._boxPointer.open(BoxPointer.PopupAnimation.FULL);
        global.stage.connectObject(
            'captured-event', this._onCapturedEvent.bind(this), this);
        this.keyButton.connectObject('notify::mapped', () => {
            if (!this.keyButton.is_mapped())
                this._hideSubkeys();
        }, this);
    }

    _hideSubkeys() {
        if (this._boxPointer)
            this._boxPointer.close(BoxPointer.PopupAnimation.FULL);
        global.stage.disconnectObject(this);
        this.keyButton.disconnectObject(this);
        this._capturedPress = false;
    }

    _makeKey(commitString, label, icon) {
        let button = new St.Button({
            style_class: 'keyboard-key',
            x_expand: true,
        });

        if (icon) {
            const child = new St.Icon({icon_name: icon});
            button.set_child(child);
            this._icon = child;
        } else if (label) {
            button.set_label(label);
        } else if (commitString) {
            button.set_label(commitString);
        }

        button.connect('button-press-event', () => {
            this._press(button, commitString);
            button.add_style_pseudo_class('active');
            return Clutter.EVENT_STOP;
        });
        button.connect('button-release-event', () => {
            this._release(button, commitString);
            button.remove_style_pseudo_class('active');
            return Clutter.EVENT_STOP;
        });
        button.connect('touch-event', (actor, event) => {
            // We only handle touch events here on wayland. On X11
            // we do get emulated pointer events, which already works
            // for single-touch cases. Besides, the X11 passive touch grab
            // set up by Mutter will make us see first the touch events
            // and later the pointer events, so it will look like two
            // unrelated series of events, we want to avoid double handling
            // in these cases.
            if (!Meta.is_wayland_compositor())
                return Clutter.EVENT_PROPAGATE;

            const slot = event.get_event_sequence().get_slot();

            if (!this._touchPressSlot &&
                event.type() === Clutter.EventType.TOUCH_BEGIN) {
                this._touchPressSlot = slot;
                this._press(button, commitString);
                button.add_style_pseudo_class('active');
            } else if (event.type() === Clutter.EventType.TOUCH_END) {
                if (!this._touchPressSlot ||
                    this._touchPressSlot === slot) {
                    this._release(button, commitString);
                    button.remove_style_pseudo_class('active');
                }

                if (this._touchPressSlot === slot)
                    this._touchPressSlot = null;
            }
            return Clutter.EVENT_STOP;
        });

        return button;
    }

    _getExtendedKeys() {
        this._extendedKeyboard = new St.BoxLayout({
            style_class: 'key-container',
            vertical: false,
        });
        for (let i = 0; i < this._extendedKeys.length; ++i) {
            let extendedKey = this._extendedKeys[i];
            let key = this._makeKey(extendedKey);

            key.extendedKey = extendedKey;
            this._extendedKeyboard.add_child(key);

            key.set_size(...this.keyButton.allocation.get_size());
            this.keyButton.connect('notify::allocation',
                () => key.set_size(...this.keyButton.allocation.get_size()));
        }
        this._boxPointer.bin.add_child(this._extendedKeyboard);
    }

    get subkeys() {
        return this._boxPointer;
    }

    setLatched(latched) {
        if (latched)
            this.keyButton.add_style_pseudo_class('latched');
        else
            this.keyButton.remove_style_pseudo_class('latched');
    }
});

class KeyboardModel {
    constructor(groupName) {
        this._model = this._loadModel(groupName);
    }

    _loadModel(groupName) {
        const file = Gio.File.new_for_uri(
            `resource:///org/gnome/shell/osk-layouts/${groupName}.json`);
        let [success_, contents] = file.load_contents(null);

        const decoder = new TextDecoder();
        return JSON.parse(decoder.decode(contents));
    }

    getLevels() {
        return this._model.levels;
    }

    getKeysForLevel(levelName) {
        return this._model.levels.find(level => level === levelName);
    }
}

class FocusTracker extends Signals.EventEmitter {
    constructor() {
        super();

        this._rect = null;

        global.display.connectObject(
            'notify::focus-window', () => {
                this._setCurrentWindow(global.display.focus_window);
                this.emit('window-changed', this._currentWindow);
            },
            'grab-op-begin', (display, window, op) => {
                if (window === this._currentWindow &&
                    (op === Meta.GrabOp.MOVING || op === Meta.GrabOp.KEYBOARD_MOVING))
                    this.emit('window-grabbed');
            }, this);

        this._setCurrentWindow(global.display.focus_window);

        /* Valid for wayland clients */
        Main.inputMethod.connectObject('cursor-location-changed',
            (o, rect) => this._setCurrentRect(rect), this);

        this._ibusManager = IBusManager.getIBusManager();
        this._ibusManager.connectObject(
            'set-cursor-location', (manager, rect) => {
                /* Valid for X11 clients only */
                if (Main.inputMethod.currentFocus)
                    return;

                const grapheneRect = new Graphene.Rect();
                grapheneRect.init(rect.x, rect.y, rect.width, rect.height);

                this._setCurrentRect(grapheneRect);
            },
            'focus-in', () => this.emit('focus-changed', true),
            'focus-out', () => this.emit('focus-changed', false),
            this);
    }

    destroy() {
        this._currentWindow?.disconnectObject(this);
        global.display.disconnectObject(this);
        Main.inputMethod.disconnectObject(this);
        this._ibusManager.disconnectObject(this);
    }

    get currentWindow() {
        return this._currentWindow;
    }

    _setCurrentWindow(window) {
        this._currentWindow?.disconnectObject(this);

        this._currentWindow = window;

        if (this._currentWindow) {
            this._currentWindow.connectObject(
                'position-changed', () => this.emit('window-moved'), this);
        }
    }

    _setCurrentRect(rect) {
        // Some clients give us 0-sized rects, in that case set size to 1
        if (rect.size.width <= 0)
            rect.size.width = 1;
        if (rect.size.height <= 0)
            rect.size.height = 1;

        if (this._currentWindow) {
            const frameRect = this._currentWindow.get_frame_rect();
            const grapheneFrameRect = new Graphene.Rect();
            grapheneFrameRect.init(frameRect.x, frameRect.y,
                frameRect.width, frameRect.height);

            const rectInsideFrameRect = grapheneFrameRect.intersection(rect)[0];
            if (!rectInsideFrameRect)
                return;
        }

        if (this._rect && this._rect.equal(rect))
            return;

        this._rect = rect;
        this.emit('position-changed');
    }

    getCurrentRect() {
        const rect = {
            x: this._rect.origin.x,
            y: this._rect.origin.y,
            width: this._rect.size.width,
            height: this._rect.size.height,
        };

        return rect;
    }
}

const EmojiPager = GObject.registerClass({
    Properties: {
        'delta': GObject.ParamSpec.int(
            'delta', 'delta', 'delta',
            GObject.ParamFlags.READWRITE,
            GLib.MININT32, GLib.MAXINT32, 0),
    },
    Signals: {
        'emoji': {param_types: [GObject.TYPE_STRING]},
        'page-changed': {
            param_types: [GObject.TYPE_INT, GObject.TYPE_INT, GObject.TYPE_INT],
        },
    },
}, class EmojiPager extends St.Widget {
    _init(sections) {
        super._init({
            layout_manager: new Clutter.BinLayout(),
            reactive: true,
            clip_to_allocation: true,
            y_expand: true,
        });
        this._sections = sections;

        this._pages = [];
        this._panel = null;
        this._curPage = null;
        this._followingPage = null;
        this._followingPanel = null;
        this._currentKey = null;
        this._delta = 0;
        this._width = null;

        const swipeTracker = new SwipeTracker.SwipeTracker(this,
            Clutter.Orientation.HORIZONTAL,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            {allowDrag: true, allowScroll: true});
        swipeTracker.connect('begin', this._onSwipeBegin.bind(this));
        swipeTracker.connect('update', this._onSwipeUpdate.bind(this));
        swipeTracker.connect('end', this._onSwipeEnd.bind(this));
        this._swipeTracker = swipeTracker;

        this.connect('destroy', () => this._onDestroy());

        this.bind_property(
            'visible', this._swipeTracker, 'enabled',
            GObject.BindingFlags.DEFAULT);
    }

    _onDestroy() {
        if (this._swipeTracker) {
            this._swipeTracker.destroy();
            delete this._swipeTracker;
        }
    }

    get delta() {
        return this._delta;
    }

    set delta(value) {
        if (this._delta === value)
            return;

        this._delta = value;
        this.notify('delta');

        let followingPage = this.getFollowingPage();

        if (this._followingPage !== followingPage) {
            if (this._followingPanel) {
                this._followingPanel.destroy();
                this._followingPanel = null;
            }

            if (followingPage != null) {
                this._followingPanel = this._generatePanel(followingPage);
                this.add_child(this._followingPanel);
            }

            this._followingPage = followingPage;
        }

        const multiplier = this.text_direction === Clutter.TextDirection.RTL
            ? -1 : 1;

        this._panel.translation_x = value * multiplier;
        if (this._followingPanel) {
            const translation = value < 0
                ? this._width + EMOJI_PAGE_SEPARATION
                : -this._width - EMOJI_PAGE_SEPARATION;

            this._followingPanel.translation_x =
                (value * multiplier) + (translation * multiplier);
        }
    }

    _prevPage(nPage) {
        return (nPage + this._pages.length - 1) % this._pages.length;
    }

    _nextPage(nPage) {
        return (nPage + 1) % this._pages.length;
    }

    getFollowingPage() {
        if (this.delta === 0)
            return null;

        if (this.delta < 0)
            return this._nextPage(this._curPage);
        else
            return this._prevPage(this._curPage);
    }

    _onSwipeUpdate(tracker, progress) {
        this.delta = -progress * this._width;

        if (this._currentKey != null) {
            this._currentKey.cancel();
            this._currentKey = null;
        }

        return false;
    }

    _onSwipeBegin(tracker) {
        this._width = this.width;
        const points = [-1, 0, 1];
        tracker.confirmSwipe(this._width, points, 0, 0);
    }

    _onSwipeEnd(tracker, duration, endProgress) {
        this.remove_all_transitions();
        if (endProgress === 0) {
            this.ease_property('delta', 0, {duration});
        } else {
            const value = endProgress < 0
                ? this._width + EMOJI_PAGE_SEPARATION
                : -this._width - EMOJI_PAGE_SEPARATION;
            this.ease_property('delta', value, {
                duration,
                onComplete: () => {
                    this.setCurrentPage(this.getFollowingPage());
                },
            });
        }
    }

    _initPagingInfo() {
        this._pages = [];

        for (let i = 0; i < this._sections.length; i++) {
            let section = this._sections[i];
            let itemsPerPage = this._nCols * this._nRows;
            let nPages = Math.ceil(section.keys.length / itemsPerPage);
            let page = -1;
            let pageKeys;

            for (let j = 0; j < section.keys.length; j++) {
                if (j % itemsPerPage === 0) {
                    page++;
                    pageKeys = [];
                    this._pages.push({pageKeys, nPages, page, section: this._sections[i]});
                }

                pageKeys.push(section.keys[j]);
            }
        }
    }

    _lookupSection(section, nPage) {
        for (let i = 0; i < this._pages.length; i++) {
            let page = this._pages[i];

            if (page.section === section && page.page === nPage)
                return i;
        }

        return -1;
    }

    _generatePanel(nPage) {
        const gridLayout = new Clutter.GridLayout({
            orientation: Clutter.Orientation.HORIZONTAL,
            column_homogeneous: true,
            row_homogeneous: true,
        });
        const panel = new St.Widget({
            layout_manager: gridLayout,
            style_class: 'emoji-page',
            x_expand: true,
            y_expand: true,
        });

        /* Set an expander actor so all proportions are right despite the panel
         * not having all rows/cols filled in.
         */
        let expander = new Clutter.Actor();
        gridLayout.attach(expander, 0, 0, this._nCols, this._nRows);

        let page = this._pages[nPage];
        let col = 0;
        let row = 0;

        for (let i = 0; i < page.pageKeys.length; i++) {
            let modelKey = page.pageKeys[i];
            let key = new Key({commitString: modelKey.label}, modelKey.variants);

            key.keyButton.set_button_mask(0);

            key.connect('pressed', () => {
                this._currentKey = key;
            });
            key.connect('commit', (actor, str) => {
                if (this._currentKey !== key)
                    return;
                this._currentKey = null;
                this.emit('emoji', str);
            });

            gridLayout.attach(key, col, row, 1, 1);

            col++;
            if (col >= this._nCols) {
                col = 0;
                row++;
            }
        }

        return panel;
    }

    setCurrentPage(nPage) {
        if (this._curPage === nPage)
            return;

        this._curPage = nPage;

        if (this._panel) {
            this._panel.destroy();
            this._panel = null;
        }

        /* Reuse followingPage if possible */
        if (nPage === this._followingPage) {
            this._panel = this._followingPanel;
            this._followingPanel = null;
        }

        if (this._followingPanel)
            this._followingPanel.destroy();

        this._followingPanel = null;
        this._followingPage = null;
        this._delta = 0;

        if (!this._panel) {
            this._panel = this._generatePanel(nPage);
            this.add_child(this._panel);
        }

        let page = this._pages[nPage];
        this.emit('page-changed', page.section.label, page.page, page.nPages);
    }

    setCurrentSection(section, nPage) {
        for (let i = 0; i < this._pages.length; i++) {
            let page = this._pages[i];

            if (page.section === section && page.page === nPage) {
                this.setCurrentPage(i);
                break;
            }
        }
    }

    setRatio(nCols, nRows) {
        this._nCols = nCols;
        this._nRows = nRows;
        this._initPagingInfo();
    }
});

const EmojiSelection = GObject.registerClass({
    Signals: {
        'emoji-selected': {param_types: [GObject.TYPE_STRING]},
        'close-request': {},
        'toggle': {},
    },
}, class EmojiSelection extends St.Widget {
    _init() {
        const gridLayout = new Clutter.GridLayout({
            orientation: Clutter.Orientation.HORIZONTAL,
            column_homogeneous: true,
            row_homogeneous: true,
        });
        super._init({
            layout_manager: gridLayout,
            style_class: 'emoji-panel',
            x_expand: true,
            y_expand: true,
            text_direction: global.stage.text_direction,
        });

        this._sections = [
            {first: 'grinning face', label: '🙂️'},
            {first: 'selfie', label: '👍️'},
            {first: 'monkey face', label: '🌷️'},
            {first: 'grapes', label: '🍴️'},
            {first: 'globe showing Europe-Africa', label: '✈️'},
            {first: 'jack-o-lantern', label: '🏃️'},
            {first: 'muted speaker', label: '🔔️'},
            {first: 'ATM sign', label: '❤️'},
            {first: 'chequered flag', label: '🚩️'},
        ];

        this._gridLayout = gridLayout;
        this._populateSections();

        this._pagerBox = new Clutter.Actor({
            layout_manager: new Clutter.BoxLayout({
                orientation: Clutter.Orientation.VERTICAL,
            }),
        });

        this._emojiPager = new EmojiPager(this._sections);
        this._emojiPager.connect('page-changed', (pager, sectionLabel, page, nPages) => {
            this._onPageChanged(sectionLabel, page, nPages);
        });
        this._emojiPager.connect('emoji', (pager, str) => {
            this.emit('emoji-selected', str);
        });
        this._pagerBox.add_child(this._emojiPager);

        this._pageIndicator = new PageIndicators.PageIndicators(
            Clutter.Orientation.HORIZONTAL);
        this._pageIndicator.y_expand = false;
        this._pageIndicator.y_align = Clutter.ActorAlign.START;
        this._pagerBox.add_child(this._pageIndicator);
        this._pageIndicator.setReactive(false);

        this._emojiPager.connect('notify::delta', () => {
            this._updateIndicatorPosition();
        });

        this._bottomRow = this._createBottomRow();

        this._curPage = 0;
    }

    vfunc_map() {
        this._emojiPager.setCurrentPage(0);
        super.vfunc_map();
    }

    _onPageChanged(sectionLabel, page, nPages) {
        this._curPage = page;
        this._pageIndicator.setNPages(nPages);
        this._updateIndicatorPosition();

        for (let i = 0; i < this._sections.length; i++) {
            let sect = this._sections[i];
            sect.button.setLatched(sectionLabel === sect.label);
        }
    }

    _updateIndicatorPosition() {
        this._pageIndicator.setCurrentPosition(this._curPage -
            this._emojiPager.delta / this._emojiPager.width);
    }

    _findSection(emoji) {
        for (let i = 0; i < this._sections.length; i++) {
            if (this._sections[i].first === emoji)
                return this._sections[i];
        }

        return null;
    }

    _populateSections() {
        let file = Gio.File.new_for_uri('resource:///org/gnome/shell/osk-layouts/emoji.json');
        let [success_, contents] = file.load_contents(null);

        let emoji = JSON.parse(new TextDecoder().decode(contents));

        let variants = [];
        let currentKey = 0;
        let currentSection = null;

        for (let i = 0; i < emoji.length; i++) {
            /* Group variants of a same emoji so they appear on the key popover */
            if (emoji[i].name.startsWith(emoji[currentKey].name)) {
                variants.push(emoji[i].char);
                if (i < emoji.length - 1)
                    continue;
            }

            let newSection = this._findSection(emoji[currentKey].name);
            if (newSection != null) {
                currentSection = newSection;
                currentSection.keys = [];
            }

            /* Create the key */
            let label = emoji[currentKey].char + String.fromCharCode(0xFE0F);
            currentSection.keys.push({label, variants});
            currentKey = i;
            variants = [];
        }
    }

    _createBottomRow() {
        let row = new KeyContainer();
        let key;

        row.appendRow();

        key = new Key({label: 'ABC'}, []);
        key.keyButton.add_style_class_name('default-key');
        key.connect('released', () => this.emit('toggle'));
        row.appendKey(key, 1.5);

        for (let i = 0; i < this._sections.length; i++) {
            let section = this._sections[i];

            key = new Key({label: section.label}, []);
            key.connect('released', () => this._emojiPager.setCurrentSection(section, 0));
            row.appendKey(key);

            section.button = key;
        }

        key = new Key({iconName: 'keyboard-hide-symbolic'});
        key.keyButton.add_style_class_name('default-key');
        key.keyButton.add_style_class_name('hide-key');
        key.connect('released', () => {
            this.emit('close-request');
        });
        row.appendKey(key);

        const actor = new AspectContainer({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
        });
        actor.add_child(row);

        return actor;
    }

    setRatio(nCols, nRows) {
        this._emojiPager.setRatio(Math.floor(nCols), Math.floor(nRows) - 1);
        this._bottomRow.setRatio(nCols, 1);

        // (Re)attach actors so the emoji panel fits the ratio and
        // the bottom row is ensured to take 1 row high.
        if (this._pagerBox.get_parent())
            this.remove_child(this._pagerBox);
        if (this._bottomRow.get_parent())
            this.remove_child(this._bottomRow);

        this._gridLayout.attach(this._pagerBox, 0, 0, 1, Math.floor(nRows) - 1);
        this._gridLayout.attach(this._bottomRow, 0, Math.floor(nRows) - 1, 1, 1);
    }
});

export class KeyboardManager extends Signals.EventEmitter {
    constructor() {
        super();

        this._keyboard = null;
        this._a11yApplicationsSettings = new Gio.Settings({schema_id: A11Y_APPLICATIONS_SCHEMA});
        this._a11yApplicationsSettings.connect('changed', this._syncEnabled.bind(this));

        this._seat = Clutter.get_default_backend().get_default_seat();
        this._seat.connect('notify::touch-mode', this._syncEnabled.bind(this));

        this._lastDevice = null;
        global.backend.connect('last-device-changed', (backend, device) => {
            if (device.device_type === Clutter.InputDeviceType.KEYBOARD_DEVICE)
                return;

            this._lastDevice = device;
            this._syncEnabled();
        });

        const mode = Shell.ActionMode.ALL & ~Shell.ActionMode.LOCK_SCREEN;
        const bottomDragAction = new EdgeDragAction.EdgeDragAction(St.Side.BOTTOM, mode);
        bottomDragAction.connect('activated', () => {
            if (this._keyboard)
                this._keyboard.gestureActivate(Main.layoutManager.bottomIndex);
        });
        bottomDragAction.connect('progress', (_action, progress) => {
            if (this._keyboard)
                this._keyboard.gestureProgress(progress);
        });
        bottomDragAction.connect('gesture-cancel', () => {
            if (this._keyboard)
                this._keyboard.gestureCancel();
        });
        global.stage.add_action_full('osk', Clutter.EventPhase.CAPTURE, bottomDragAction);
        this._bottomDragAction = bottomDragAction;

        this._syncEnabled();
    }

    _lastDeviceIsTouchscreen() {
        if (!this._lastDevice)
            return false;

        let deviceType = this._lastDevice.get_device_type();
        return deviceType === Clutter.InputDeviceType.TOUCHSCREEN_DEVICE;
    }

    _syncEnabled() {
        let enableKeyboard = this._a11yApplicationsSettings.get_boolean(SHOW_KEYBOARD);
        let autoEnabled = this._seat.get_touch_mode() && this._lastDeviceIsTouchscreen();
        let enabled = enableKeyboard || autoEnabled;

        if (!enabled && !this._keyboard)
            return;

        if (enabled && !this._keyboard) {
            this._keyboard = new Keyboard();
            this._keyboard.connect('visibility-changed', () => {
                this.emit('visibility-changed');
                this._bottomDragAction.enabled = !this._keyboard.visible;
            });
        } else if (!enabled && this._keyboard) {
            this._keyboard.setCursorLocation(null);
            this._keyboard.destroy();
            this._keyboard = null;
            this._bottomDragAction.enabled = true;
        }
    }

    get keyboardActor() {
        return this._keyboard;
    }

    get visible() {
        return this._keyboard && this._keyboard.visible;
    }

    open(monitor) {
        Main.layoutManager.keyboardIndex = monitor;

        if (this._keyboard)
            this._keyboard.open();
    }

    close() {
        if (this._keyboard)
            this._keyboard.close();
    }

    addSuggestion(text, callback) {
        if (this._keyboard)
            this._keyboard.addSuggestion(text, callback);
    }

    resetSuggestions() {
        if (this._keyboard)
            this._keyboard.resetSuggestions();
    }

    setSuggestionsVisible(visible) {
        this._keyboard?.setSuggestionsVisible(visible);
    }

    maybeHandleEvent(event) {
        if (!this._keyboard)
            return false;

        const actor = global.stage.get_event_actor(event);

        if (Main.layoutManager.keyboardBox.contains(actor) ||
            !!actor._extendedKeys || !!actor.extendedKey) {
            actor.event(event, true);
            actor.event(event, false);
            return true;
        }

        return false;
    }
}

export const Keyboard = GObject.registerClass({
    Signals: {
        'visibility-changed': {},
    },
}, class Keyboard extends St.BoxLayout {
    _init() {
        super._init({
            name: 'keyboard',
            reactive: true,
            // Keyboard models are defined in LTR, we must override
            // the locale setting in order to avoid flipping the
            // keyboard on RTL locales.
            text_direction: Clutter.TextDirection.LTR,
            vertical: true,
        });
        this._focusInExtendedKeys = false;
        this._emojiActive = false;

        this._languagePopup = null;
        this._focusWindow = null;
        this._focusWindowStartY = null;

        this._latched = false; // current level is latched
        this._modifiers = new Set();
        this._modifierKeys = new Map();

        this._suggestions = null;

        this._focusTracker = new FocusTracker();
        this._focusTracker.connectObject(
            'position-changed', this._onFocusPositionChanged.bind(this),
            'window-grabbed', this._onFocusWindowMoving.bind(this), this);

        this._windowMovedId = this._focusTracker.connect('window-moved',
            this._onFocusWindowMoving.bind(this));

        // Valid only for X11
        if (!Meta.is_wayland_compositor()) {
            this._focusTracker.connectObject('focus-changed', (_tracker, focused) => {
                if (focused)
                    this.open(Main.layoutManager.focusIndex);
                else
                    this.close();
            }, this);
        }

        this._showIdleId = 0;

        this._keyboardVisible = false;
        this._keyboardRequested = false;
        this._keyboardRestingId = 0;

        Main.layoutManager.connectObject('monitors-changed',
            this._relayout.bind(this), this);

        this._setupKeyboard();

        this.connect('destroy', this._onDestroy.bind(this));
    }

    get visible() {
        return this._keyboardVisible && super.visible;
    }

    set visible(visible) {
        super.visible = visible;
    }

    _onFocusPositionChanged(focusTracker) {
        let rect = focusTracker.getCurrentRect();
        this.setCursorLocation(focusTracker.currentWindow, rect.x, rect.y, rect.width, rect.height);
        this._updateLevelFromHints();
    }

    _onDestroy() {
        if (this._windowMovedId) {
            this._focusTracker.disconnect(this._windowMovedId);
            delete this._windowMovedId;
        }

        if (this._focusTracker) {
            this._focusTracker.destroy();
            delete this._focusTracker;
        }

        this._clearShowIdle();

        this._keyboardController.oskCompletion = false;
        this._keyboardController.destroy();

        Main.layoutManager.untrackChrome(this);
        Main.layoutManager.keyboardBox.remove_child(this);
        Main.layoutManager.keyboardBox.hide();

        if (this._languagePopup) {
            this._languagePopup.destroy();
            this._languagePopup = null;
        }
    }

    _setupKeyboard() {
        Main.layoutManager.keyboardBox.add_child(this);
        Main.layoutManager.trackChrome(this);

        this._keyboardController = new KeyboardController();

        this._currentPage = null;

        this._suggestions = new Suggestions();
        this.add_child(this._suggestions);

        this._aspectContainer = new AspectContainer({
            layout_manager: new Clutter.BinLayout(),
            y_expand: true,
        });
        this.add_child(this._aspectContainer);

        this._emojiSelection = new EmojiSelection();
        this._emojiSelection.connect('toggle', this._toggleEmoji.bind(this));
        this._emojiSelection.connect('close-request', () => this.close(true));
        this._emojiSelection.connect('emoji-selected', (selection, emoji) => {
            this._keyboardController.commit(emoji).catch(console.error);
        });

        this._emojiSelection.hide();
        this._aspectContainer.add_child(this._emojiSelection);

        this._updateKeys();

        this._keyboardController.connectObject(
            'group-changed', this._onGroupChanged.bind(this),
            'panel-state', this._onKeyboardStateChanged.bind(this),
            'purpose-changed', this._onPurposeChanged.bind(this),
            'content-hints-changed', this._onContentHintsChanged.bind(this),
            this);
        global.stage.connectObject('notify::key-focus',
            this._onKeyFocusChanged.bind(this), this);

        this._relayout();
    }

    _onPurposeChanged(controller, purpose) {
        this._purpose = purpose;
        this._updateKeys();
    }

    _onContentHintsChanged(controller, contentHint) {
        this._contentHint = contentHint;
        this._updateLevelFromHints();
    }

    _updateLevelFromHints() {
        // If the latch is enabled, avoid level changes
        if (this._latched)
            return;

        if ((this._contentHint & Clutter.InputContentHintFlags.LOWERCASE) !== 0) {
            this._setActiveLevel('default');
            return;
        }

        if (!this._layers['shift'])
            return;

        if ((this._contentHint & Clutter.InputContentHintFlags.UPPERCASE) !== 0) {
            this._setActiveLevel('shift');
        } else if (!this._surroundingTextId &&
                   (this._contentHint &
                    (Clutter.InputContentHintFlags.AUTO_CAPITALIZATION |
                     Clutter.InputContentHintFlags.TITLECASE)) !== 0) {
            this._surroundingTextId =
                Main.inputMethod.connect('surrounding-text-set', () => {
                    const [text, cursor] = Main.inputMethod.getSurroundingText();
                    if (!text || cursor === 0) {
                        // First character in the buffer
                        this._setActiveLevel('shift');
                        return;
                    }

                    const beforeCursor = GLib.utf8_substring(text, 0, cursor);

                    if ((this._contentHint & Clutter.InputContentHintFlags.TITLECASE) !== 0) {
                        if (beforeCursor.charAt(beforeCursor.length - 1) === ' ')
                            this._setActiveLevel('shift');
                        else
                            this._setActiveLevel('default');
                    } else if ((this._contentHint & Clutter.InputContentHintFlags.AUTO_CAPITALIZATION) !== 0) {
                        if (beforeCursor.charAt(beforeCursor.trimEnd().length - 1) === '.')
                            this._setActiveLevel('shift');
                        else
                            this._setActiveLevel('default');
                    }

                    Main.inputMethod.disconnect(this._surroundingTextId);
                    this._surroundingTextId = 0;
                });
            Main.inputMethod.request_surrounding();
        }
    }

    _onKeyFocusChanged() {
        let focus = global.stage.key_focus;

        // Showing an extended key popup and clicking a key from the extended keys
        // will grab focus, but ignore that
        let extendedKeysWereFocused = this._focusInExtendedKeys;
        this._focusInExtendedKeys = focus && (focus._extendedKeys || focus.extendedKey);
        if (this._focusInExtendedKeys || extendedKeysWereFocused)
            return;

        if (!(focus instanceof Clutter.Text)) {
            this.close();
            return;
        }

        if (!this._showIdleId) {
            this._showIdleId = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
                this.open(Main.layoutManager.focusIndex);
                this._showIdleId = 0;
                return GLib.SOURCE_REMOVE;
            });
            GLib.Source.set_name_by_id(this._showIdleId, '[gnome-shell] this.open');
        }
    }

    _updateLayout(groupName, purpose) {
        let keyboardModel = null;
        let layers = {};
        let layout = new Clutter.Actor({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
        });

        if (purpose === Clutter.InputContentPurpose.DIGITS) {
            keyboardModel = new KeyboardModel('digits');
        } else if (purpose === Clutter.InputContentPurpose.NUMBER) {
            keyboardModel = new KeyboardModel('number');
        } else if (purpose === Clutter.InputContentPurpose.PHONE) {
            keyboardModel = new KeyboardModel('phone');
        } else if (purpose === Clutter.InputContentPurpose.EMAIL) {
            keyboardModel = new KeyboardModel('email');
        } else if (purpose === Clutter.InputContentPurpose.URL) {
            keyboardModel = new KeyboardModel('url');
        } else {
            let groups = [groupName];
            if (groupName.includes('+'))
                groups.push(groupName.replace(/\+.*/, ''));
            groups.push('us');

            if (purpose === Clutter.InputContentPurpose.TERMINAL)
                groups = groups.map(g => `${g}-extended`);

            for (const group of groups) {
                try {
                    keyboardModel = new KeyboardModel(group);
                    break;
                } catch (e) {
                    // Ignore this error and fall back to next model
                }
            }

            if (!keyboardModel)
                return;
        }

        const emojiVisible = Meta.is_wayland_compositor() &&
            (purpose === Clutter.InputContentPurpose.NORMAL ||
             purpose === Clutter.InputContentPurpose.ALPHA ||
             purpose === Clutter.InputContentPurpose.PASSWORD ||
             purpose === Clutter.InputContentPurpose.TERMINAL);

        keyboardModel.getLevels().forEach(currentLevel => {
            let levelLayout = new KeyContainer();
            levelLayout.shiftKeys = [];
            levelLayout.mode = currentLevel.mode;

            const rows = currentLevel.rows;
            rows.forEach(row => {
                levelLayout.appendRow();
                this._addRowKeys(row, levelLayout, emojiVisible);
            });

            layers[currentLevel.level] = levelLayout;
            layout.add_child(levelLayout);
            levelLayout.hide();
        });

        this._aspectContainer.add_child(layout);
        this._currentLayout?.destroy();
        this._currentLayout = layout;
        this._layers = layers;
    }

    _addRowKeys(keys, layout, emojiVisible) {
        let accumulatedWidth = 0;
        for (let i = 0; i < keys.length; ++i) {
            const key = keys[i];
            const {strings} = key;
            const commitString = strings?.shift();

            if (key.action === 'emoji' && !emojiVisible) {
                accumulatedWidth = key.width ?? 1;
                continue;
            }

            if (accumulatedWidth > 0) {
                // Pass accumulated width onto the next key
                key.width = (key.width ?? 1) + accumulatedWidth;
                accumulatedWidth = 0;
            }

            let button = new Key({
                commitString,
                label: key.label,
                iconName: key.iconName,
                keyval: key.keyval,
            }, strings);

            if (key.keyval) {
                button.connect('keyval', (_actor, keyval) => {
                    this._keyboardController.keyvalPress(keyval);
                    this._keyboardController.keyvalRelease(keyval);
                });
            }

            if (key.action !== 'modifier') {
                button.connect('commit', (_actor, str) => {
                    this._keyboardController.commit(str, this._modifiers).then(() => {
                        this._disableAllModifiers();
                        if (layout.mode === 'default' ||
                            (layout.mode === 'latched' && !this._latched)) {
                            if (this._contentHint !== 0)
                                this._updateLevelFromHints();
                            else
                                this._setActiveLevel('default');
                        }
                    }).catch(console.error);
                });
            }

            if (key.action !== null) {
                button.connect('released', () => {
                    if (key.action === 'hide') {
                        this.close(true);
                    } else if (key.action === 'languageMenu') {
                        this._popupLanguageMenu(button);
                    } else if (key.action === 'emoji') {
                        this._toggleEmoji();
                    } else if (key.action === 'modifier') {
                        this._toggleModifier(key.keyval);
                    } else if (key.action === 'delete') {
                        this._keyboardController.toggleDelete(true);
                        this._keyboardController.toggleDelete(false);
                    } else if (!this._longPressed && key.action === 'levelSwitch') {
                        this._setActiveLevel(key.level);
                        this._setLatched(
                            key.level === 1 &&
                                key.iconName === 'keyboard-caps-lock-symbolic');
                    }

                    this._longPressed = false;
                });
            }

            if (key.action === 'levelSwitch' &&
                key.iconName === 'keyboard-shift-symbolic') {
                layout.shiftKeys.push(button);
                if (key.level === 'shift') {
                    button.connect('long-press', () => {
                        this._setActiveLevel(key.level);
                        this._setLatched(true);
                        this._longPressed = true;
                    });
                }
            }

            if (key.action === 'delete') {
                button.connect('long-press',
                    () => this._keyboardController.toggleDelete(true));
            }

            if (key.action === 'modifier') {
                let modifierKeys = this._modifierKeys[key.keyval] || [];
                modifierKeys.push(button);
                this._modifierKeys[key.keyval] = modifierKeys;
            }

            if (key.action || key.keyval)
                button.keyButton.add_style_class_name('default-key');

            layout.appendKey(button, key.width, key.height, key.leftOffset);
        }
    }

    _setLatched(latched) {
        this._latched = latched;
        this._setCurrentLevelLatched(this._currentPage, this._latched);
    }

    _setModifierEnabled(keyval, enabled) {
        if (enabled)
            this._modifiers.add(keyval);
        else
            this._modifiers.delete(keyval);

        for (const key of this._modifierKeys[keyval])
            key.setLatched(enabled);
    }

    _toggleModifier(keyval) {
        const isActive = this._modifiers.has(keyval);
        this._setModifierEnabled(keyval, !isActive);
    }

    _disableAllModifiers() {
        for (const keyval of this._modifiers)
            this._setModifierEnabled(keyval, false);
    }

    _popupLanguageMenu(keyActor) {
        if (this._languagePopup)
            this._languagePopup.destroy();

        this._languagePopup = new LanguageSelectionPopup(keyActor);
        Main.layoutManager.addTopChrome(this._languagePopup.actor);
        this._languagePopup.open(true);
    }

    _updateCurrentPageVisible() {
        if (this._currentPage)
            this._currentPage.visible = !this._emojiActive;
    }

    _setEmojiActive(active) {
        this._emojiActive = active;
        this._emojiSelection.visible = this._emojiActive;
        this._updateCurrentPageVisible();
    }

    _toggleEmoji() {
        this._setEmojiActive(!this._emojiActive);
    }

    _setCurrentLevelLatched(layout, latched) {
        for (let i = 0; i < layout.shiftKeys.length; i++) {
            let key = layout.shiftKeys[i];
            key.setLatched(latched);
            key.iconName = latched
                ? 'keyboard-caps-lock-symbolic' : 'keyboard-shift-symbolic';
        }
    }

    _getGridSlots() {
        let numOfHorizSlots = 0, numOfVertSlots;
        let rows = this._currentPage.get_children();
        numOfVertSlots = rows.length;

        for (let i = 0; i < rows.length; ++i) {
            let keyboardRow = rows[i];
            let keys = keyboardRow.get_children();

            numOfHorizSlots = Math.max(numOfHorizSlots, keys.length);
        }

        return [numOfHorizSlots, numOfVertSlots];
    }

    _relayout() {
        let monitor = Main.layoutManager.keyboardMonitor;

        if (!monitor)
            return;

        this.width = monitor.width;

        if (monitor.width > monitor.height)
            this.height = monitor.height / 3;
        else
            this.height = monitor.height / 4;
    }

    _updateKeys() {
        const group = this._keyboardController.getCurrentGroup();
        this._updateLayout(group, this._purpose);
        this._setActiveLevel('default');
    }

    _onGroupChanged() {
        this._updateKeys();
    }

    _onKeyboardStateChanged(controller, state) {
        let enabled;
        if (state === Clutter.InputPanelState.OFF)
            enabled = false;
        else if (state === Clutter.InputPanelState.ON)
            enabled = true;
        else if (state === Clutter.InputPanelState.TOGGLE)
            enabled = this._keyboardVisible === false;
        else
            return;

        if (enabled)
            this.open(Main.layoutManager.focusIndex);
        else
            this.close(true);
    }

    _setActiveLevel(activeLevel) {
        const layers = this._layers;
        let currentPage = layers[activeLevel];

        if (this._currentPage === currentPage) {
            this._updateCurrentPageVisible();
            return;
        }

        if (this._currentPage != null) {
            this._setCurrentLevelLatched(this._currentPage, false);
            this._currentPage.disconnect(this._currentPage._destroyID);
            this._currentPage.hide();
            delete this._currentPage._destroyID;
        }

        this._disableAllModifiers();
        this._currentPage = currentPage;
        this._currentPage._destroyID = this._currentPage.connect('destroy', () => {
            this._currentPage = null;
        });
        this._updateCurrentPageVisible();
        this._aspectContainer.setRatio(...this._currentPage.getRatio());
        this._emojiSelection.setRatio(...this._currentPage.getRatio());
    }

    _clearKeyboardRestTimer() {
        if (!this._keyboardRestingId)
            return;
        GLib.source_remove(this._keyboardRestingId);
        this._keyboardRestingId = 0;
    }

    open(immediate = false) {
        this._clearShowIdle();
        this._keyboardRequested = true;

        if (this._keyboardVisible) {
            this._relayout();
            return;
        }

        this._keyboardController.oskCompletion = true;
        this._clearKeyboardRestTimer();

        if (immediate) {
            this._open();
            return;
        }

        this._keyboardRestingId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
            KEYBOARD_REST_TIME,
            () => {
                this._clearKeyboardRestTimer();
                this._open();
                return GLib.SOURCE_REMOVE;
            });
        GLib.Source.set_name_by_id(this._keyboardRestingId, '[gnome-shell] this._clearKeyboardRestTimer');
    }

    _open() {
        if (!this._keyboardRequested)
            return;

        this._relayout();
        this._animateShow();

        this._setEmojiActive(false);
    }

    close(immediate = false) {
        this._clearShowIdle();
        this._keyboardRequested = false;

        if (!this._keyboardVisible)
            return;

        this._keyboardController.oskCompletion = false;
        this._clearKeyboardRestTimer();

        if (immediate) {
            this._close();
            return;
        }

        this._keyboardRestingId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
            KEYBOARD_REST_TIME,
            () => {
                this._clearKeyboardRestTimer();
                this._close();
                return GLib.SOURCE_REMOVE;
            });
        GLib.Source.set_name_by_id(this._keyboardRestingId, '[gnome-shell] this._clearKeyboardRestTimer');
    }

    _close() {
        if (this._keyboardRequested)
            return;

        this._animateHide();
        this.setCursorLocation(null);
        this._disableAllModifiers();
    }

    _animateShow() {
        if (this._focusWindow)
            this._animateWindow(this._focusWindow, true);

        Main.layoutManager.keyboardBox.show();
        this.ease({
            translation_y: -this.height,
            opacity: 255,
            duration: KEYBOARD_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this._animateShowComplete();
            },
        });
        this._keyboardVisible = true;
        this.emit('visibility-changed');
    }

    _animateShowComplete() {
        let keyboardBox = Main.layoutManager.keyboardBox;
        this._keyboardHeightNotifyId = keyboardBox.connect('notify::height', () => {
            this.translation_y = -this.height;
        });

        // Toggle visibility so the keyboardBox can update its chrome region.
        if (!Meta.is_wayland_compositor()) {
            keyboardBox.hide();
            keyboardBox.show();
        }
    }

    _animateHide() {
        if (this._focusWindow)
            this._animateWindow(this._focusWindow, false);

        if (this._keyboardHeightNotifyId) {
            Main.layoutManager.keyboardBox.disconnect(this._keyboardHeightNotifyId);
            this._keyboardHeightNotifyId = 0;
        }
        this.ease({
            translation_y: 0,
            opacity: 0,
            duration: KEYBOARD_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_IN_QUAD,
            onComplete: () => {
                this._animateHideComplete();
            },
        });

        this._keyboardVisible = false;
        this.emit('visibility-changed');
    }

    _animateHideComplete() {
        Main.layoutManager.keyboardBox.hide();
    }

    gestureProgress(delta) {
        this._gestureInProgress = true;
        Main.layoutManager.keyboardBox.show();
        let progress = Math.min(delta, this.height) / this.height;
        this.translation_y = -this.height * progress;
        this.opacity = 255 * progress;
        const windowActor = this._focusWindow?.get_compositor_private();
        if (windowActor)
            windowActor.y = this._focusWindowStartY - (this.height * progress);
    }

    gestureActivate() {
        this.open(true);
        this._gestureInProgress = false;
    }

    gestureCancel() {
        if (this._gestureInProgress)
            this._animateHide();
        this._gestureInProgress = false;
    }

    resetSuggestions() {
        if (this._suggestions)
            this._suggestions.clear();
    }

    setSuggestionsVisible(visible) {
        this._suggestions?.setVisible(visible);
    }

    addSuggestion(text, callback) {
        if (!this._suggestions)
            return;
        this._suggestions.add(text, callback);
        this._suggestions.show();
    }

    _clearShowIdle() {
        if (!this._showIdleId)
            return;
        GLib.source_remove(this._showIdleId);
        this._showIdleId = 0;
    }

    _windowSlideAnimationComplete(window, finalY) {
        // Synchronize window positions again.
        const frameRect = window.get_frame_rect();
        const bufferRect = window.get_buffer_rect();

        finalY += frameRect.y - bufferRect.y;

        frameRect.y = finalY;

        this._focusTracker.disconnect(this._windowMovedId);
        window.move_frame(true, frameRect.x, frameRect.y);
        this._windowMovedId = this._focusTracker.connect('window-moved',
            this._onFocusWindowMoving.bind(this));
    }

    _animateWindow(window, show) {
        let windowActor = window.get_compositor_private();
        if (!windowActor)
            return;

        const finalY = show
            ? this._focusWindowStartY - Main.layoutManager.keyboardBox.height
            : this._focusWindowStartY;

        windowActor.ease({
            y: finalY,
            duration: KEYBOARD_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onStopped: () => {
                windowActor.y = finalY;
                this._windowSlideAnimationComplete(window, finalY);
            },
        });
    }

    _onFocusWindowMoving() {
        if (this._focusTracker.currentWindow === this._focusWindow) {
            // Don't use _setFocusWindow() here because that would move the
            // window while the user has grabbed it. Instead we simply "let go"
            // of the window.
            this._focusWindow = null;
            this._focusWindowStartY = null;
        }

        this.close(true);
    }

    _setFocusWindow(window) {
        if (this._focusWindow === window)
            return;

        if (this._keyboardVisible && this._focusWindow)
            this._animateWindow(this._focusWindow, false);

        const windowActor = window?.get_compositor_private();
        windowActor?.remove_transition('y');
        this._focusWindowStartY = windowActor ? windowActor.y : null;

        if (this._keyboardVisible && window)
            this._animateWindow(window, true);

        this._focusWindow = window;
    }

    setCursorLocation(window, x, y, w, h) {
        let monitor = Main.layoutManager.keyboardMonitor;

        if (window && monitor) {
            const keyboardHeight = Main.layoutManager.keyboardBox.height;
            const keyboardY1 = (monitor.y + monitor.height) - keyboardHeight;

            if (this._focusWindow === window) {
                if (y + h + keyboardHeight < keyboardY1)
                    this._setFocusWindow(null);

                return;
            }

            if (y + h >= keyboardY1)
                this._setFocusWindow(window);
            else
                this._setFocusWindow(null);
        } else {
            this._setFocusWindow(null);
        }
    }
});

class KeyboardController extends Signals.EventEmitter {
    constructor() {
        super();

        let seat = Clutter.get_default_backend().get_default_seat();
        this._virtualDevice = seat.create_virtual_device(Clutter.InputDeviceType.KEYBOARD_DEVICE);

        this._inputSourceManager = InputSourceManager.getInputSourceManager();
        this._inputSourceManager.connectObject(
            'current-source-changed', this._onSourceChanged.bind(this),
            'sources-changed', this._onSourcesModified.bind(this), this);
        this._currentSource = this._inputSourceManager.currentSource;

        Main.inputMethod.connectObject(
            'notify::content-purpose', this._onPurposeHintsChanged.bind(this),
            'notify::content-hints', this._onContentHintsChanged.bind(this),
            'input-panel-state', (o, state) => this.emit('panel-state', state), this);
    }

    destroy() {
        this._inputSourceManager.disconnectObject(this);
        Main.inputMethod.disconnectObject(this);

        // Make sure any buttons pressed by the virtual device are released
        // immediately instead of waiting for the next GC cycle
        this._virtualDevice.run_dispose();
    }

    _onSourcesModified() {
        this.emit('group-changed');
    }

    _onSourceChanged(inputSourceManager, _oldSource) {
        let source = inputSourceManager.currentSource;
        this._currentSource = source;
        this.emit('group-changed');
    }

    _onPurposeHintsChanged(method) {
        const purpose = method.content_purpose;
        this._purpose = purpose;
        this.emit('purpose-changed', purpose);
    }

    _onContentHintsChanged(method) {
        const contentHints = method.content_hints;
        this._contentHints = contentHints;
        this.emit('content-hints-changed', contentHints);
    }

    getCurrentGroup() {
        // Special case for Korean, if Hangul mode is disabled, use the 'us' keymap
        if (this._currentSource.id === 'hangul') {
            const inputSourceManager = InputSourceManager.getInputSourceManager();
            const currentSource = inputSourceManager.currentSource;
            let prop;
            for (let i = 0; (prop = currentSource.properties.get(i)) !== null; ++i) {
                if (prop.get_key() === 'InputMode' &&
                    prop.get_prop_type() === IBus.PropType.TOGGLE &&
                    prop.get_state() !== IBus.PropState.CHECKED)
                    return 'us';
            }
        }

        return this._currentSource.xkbId;
    }

    _forwardModifiers(modifiers, type) {
        for (const keyval of modifiers) {
            if (type === Clutter.EventType.KEY_PRESS)
                this.keyvalPress(keyval);
            else if (type === Clutter.EventType.KEY_RELEASE)
                this.keyvalRelease(keyval);
        }
    }

    _getKeyvalsFromString(string) {
        const keyvals = [];
        for (const unicode of string) {
            const keyval = Clutter.unicode_to_keysym(unicode.codePointAt(0));
            // If the unicode character is unknown, try to avoid keyvals at all
            if (keyval === (unicode || 0x01000000))
                return [];

            keyvals.push(keyval);
        }

        return keyvals;
    }

    async commit(str, modifiers) {
        const keyvals = this._getKeyvalsFromString(str);

        // If there is no IM focus (e.g. with X11 clients), or modifiers
        // are in use, send raw key events.
        if (!Main.inputMethod.currentFocus || modifiers?.size > 0) {
            if (modifiers)
                this._forwardModifiers(modifiers, Clutter.EventType.KEY_PRESS);

            for (const keyval of keyvals) {
                this.keyvalPress(keyval);
                this.keyvalRelease(keyval);
            }

            if (modifiers)
                this._forwardModifiers(modifiers, Clutter.EventType.KEY_RELEASE);

            return;
        }

        // If OSK completion is enabled, or there is an active source requiring
        // IBus to receive input, prefer to feed the events directly to the IM
        if (this._oskCompletionEnabled ||
            this._currentSource.type === InputSourceManager.INPUT_SOURCE_TYPE_IBUS) {
            for (const keyval of keyvals) {
                // eslint-disable-next-line no-await-in-loop
                if (!await Main.inputMethod.handleVirtualKey(keyval)) {
                    this.keyvalPress(keyval);
                    this.keyvalRelease(keyval);
                }
            }
            return;
        }

        Main.inputMethod.commit(str);
    }

    set oskCompletion(enabled) {
        if (this._oskCompletionEnabled === enabled)
            return;

        this._oskCompletionEnabled =
            IBusManager.getIBusManager().setCompletionEnabled(enabled, () => Main.inputMethod.update());
    }

    keyvalPress(keyval) {
        this._virtualDevice.notify_keyval(Clutter.get_current_event_time() * 1000,
            keyval, Clutter.KeyState.PRESSED);
    }

    keyvalRelease(keyval) {
        this._virtualDevice.notify_keyval(Clutter.get_current_event_time() * 1000,
            keyval, Clutter.KeyState.RELEASED);
    }

    _previousWordPosition(text, cursor) {
        const upToCursor = [...text].slice(0, cursor).join('');
        const jsStringPos = Math.max(0, upToCursor.search(/\s+\S+\s*$/));
        const charPos = GLib.utf8_strlen(text.slice(0, jsStringPos), -1);
        return charPos;
    }

    toggleDelete(enabled) {
        if (this._deleteEnabled === enabled)
            return;

        this._deleteEnabled = enabled;
        this._timesDeleted = 0;

        /* If there is no IM focus or are in the middle of preedit, fallback to
         * keypresses */
        if (enabled &&
            (!Main.inputMethod.currentFocus ||
             Main.inputMethod.hasPreedit() ||
             this._purpose === Clutter.InputContentPurpose.TERMINAL)) {
            this.keyvalPress(Clutter.KEY_BackSpace);
            this._backspacePressed = true;
            return;
        }

        if (!enabled && this._backspacePressed) {
            this.keyvalRelease(Clutter.KEY_BackSpace);
            delete this._backspacePressed;
            return;
        }

        if (enabled) {
            let func = (text, cursor, anchor) => {
                if (cursor === 0 && anchor === 0)
                    return;

                let offset, len;
                if (cursor > anchor) {
                    offset = anchor - cursor;
                    len = -offset;
                } else if (cursor < anchor) {
                    offset = 0;
                    len = anchor - cursor;
                } else if (this._timesDeleted < BACKSPACE_WORD_DELETE_THRESHOLD) {
                    offset = -1;
                    len = 1;
                } else {
                    const wordLength = cursor - this._previousWordPosition(text, cursor);
                    offset = -wordLength;
                    len = wordLength;
                }

                this._timesDeleted++;
                Main.inputMethod.delete_surrounding(offset, len);
            };

            this._surroundingUpdateId = Main.inputMethod.connect(
                'surrounding-text-set', () => {
                    let [text, cursor, anchor] = Main.inputMethod.getSurroundingText();
                    if (this._timesDeleted === 0) {
                        func(text, cursor, anchor);
                    } else {
                        if (this._surroundingUpdateTimeoutId > 0) {
                            GLib.source_remove(this._surroundingUpdateTimeoutId);
                            this._surroundingUpdateTimeoutId = 0;
                        }
                        this._surroundingUpdateTimeoutId =
                            GLib.timeout_add(GLib.PRIORITY_DEFAULT, KEY_RELEASE_TIMEOUT, () => {
                                func(text, cursor, cursor);
                                this._surroundingUpdateTimeoutId = 0;
                                return GLib.SOURCE_REMOVE;
                            });
                    }
                });

            let [text, cursor, anchor] = Main.inputMethod.getSurroundingText();
            if (text)
                func(text, cursor, anchor);
            else
                Main.inputMethod.request_surrounding();
        } else {
            if (this._surroundingUpdateId > 0) {
                Main.inputMethod.disconnect(this._surroundingUpdateId);
                this._surroundingUpdateId = 0;
            }
            if (this._surroundingUpdateTimeoutId > 0) {
                GLib.source_remove(this._surroundingUpdateTimeoutId);
                this._surroundingUpdateTimeoutId = 0;
            }
        }
    }
}
(uuay)pointerWatcher.js// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GLib from 'gi://GLib';

// We stop polling if the user is idle for more than this amount of time
const IDLE_TIME = 1000;

// This file implements a reasonably efficient system for tracking the position
// of the mouse pointer. We simply query the pointer from the X server in a loop,
// but we turn off the polling when the user is idle.

let _pointerWatcher = null;

/**
 * @returns {PointerWatcher}
 */
export function getPointerWatcher() {
    if (_pointerWatcher == null)
        _pointerWatcher = new PointerWatcher();

    return _pointerWatcher;
}

class PointerWatch {
    constructor(watcher, interval, callback) {
        this.watcher = watcher;
        this.interval = interval;
        this.callback = callback;
    }

    // remove:
    // remove this watch. This function may safely be called
    // while the callback is executing.
    remove() {
        this.watcher._removeWatch(this);
    }
}

class PointerWatcher {
    constructor() {
        this._idleMonitor = global.backend.get_core_idle_monitor();
        this._idleMonitor.add_idle_watch(IDLE_TIME, this._onIdleMonitorBecameIdle.bind(this));
        this._idle = this._idleMonitor.get_idletime() > IDLE_TIME;
        this._watches = [];
        this.pointerX = null;
        this.pointerY = null;
    }

    // addWatch:
    // @interval: hint as to the time resolution needed. When the user is
    //   not idle, the position of the pointer will be queried at least
    //   once every this many milliseconds.
    // @callback to call when the pointer position changes - takes
    //   two arguments, X and Y.
    //
    // Set up a watch on the position of the mouse pointer. Returns a
    // PointerWatch object which has a remove() method to remove the watch.
    addWatch(interval, callback) {
        // Avoid unreliably calling the watch for the current position
        this._updatePointer();

        let watch = new PointerWatch(this, interval, callback);
        this._watches.push(watch);
        this._updateTimeout();
        return watch;
    }

    _removeWatch(watch) {
        for (let i = 0; i < this._watches.length; i++) {
            if (this._watches[i] === watch) {
                this._watches.splice(i, 1);
                this._updateTimeout();
                return;
            }
        }
    }

    _onIdleMonitorBecameActive() {
        this._idle = false;
        this._updatePointer();
        this._updateTimeout();
    }

    _onIdleMonitorBecameIdle() {
        this._idle = true;
        this._idleMonitor.add_user_active_watch(this._onIdleMonitorBecameActive.bind(this));
        this._updateTimeout();
    }

    _updateTimeout() {
        if (this._timeoutId) {
            GLib.source_remove(this._timeoutId);
            this._timeoutId = 0;
        }

        if (this._idle || this._watches.length === 0)
            return;

        let minInterval = this._watches[0].interval;
        for (let i = 1; i < this._watches.length; i++)
            minInterval = Math.min(this._watches[i].interval, minInterval);

        this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, minInterval,
            this._onTimeout.bind(this));
        GLib.Source.set_name_by_id(this._timeoutId, '[gnome-shell] this._onTimeout');
    }

    _onTimeout() {
        this._updatePointer();
        return GLib.SOURCE_CONTINUE;
    }

    _updatePointer() {
        let [x, y] = global.get_pointer();
        if (this.pointerX === x && this.pointerY === y)
            return;

        this.pointerX = x;
        this.pointerY = y;

        for (let i = 0; i < this._watches.length;) {
            let watch = this._watches[i];
            watch.callback(x, y);
            if (watch === this._watches[i]) // guard against self-removal
                i++;
        }
    }
}
(uuay)runDialog.js�"// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Dialog from './dialog.js';
import * as Main from './main.js';
import * as ModalDialog from './modalDialog.js';
import * as ShellEntry from './shellEntry.js';
import * as Util from '../misc/util.js';
import * as History from '../misc/history.js';

const HISTORY_KEY = 'command-history';

const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown';
const DISABLE_COMMAND_LINE_KEY = 'disable-command-line';

const TERMINAL_SCHEMA = 'org.gnome.desktop.default-applications.terminal';
const EXEC_KEY = 'exec';
const EXEC_ARG_KEY = 'exec-arg';

export const RunDialog = GObject.registerClass(
class RunDialog extends ModalDialog.ModalDialog {
    _init() {
        super._init({
            styleClass: 'run-dialog',
            destroyOnClose: false,
        });

        this._lockdownSettings = new Gio.Settings({schema_id: LOCKDOWN_SCHEMA});
        this._terminalSettings = new Gio.Settings({schema_id: TERMINAL_SCHEMA});
        global.settings.connect('changed::development-tools', () => {
            this._enableInternalCommands = global.settings.get_boolean('development-tools');
        });
        this._enableInternalCommands = global.settings.get_boolean('development-tools');

        this._internalCommands = {
            'lg': () => Main.createLookingGlass().open(),

            'r': this._restart.bind(this),

            // Developer brain backwards compatibility
            'restart': this._restart.bind(this),

            'debugexit': () => global.context.terminate(),

            // rt is short for "reload theme"
            'rt': () => {
                Main.reloadThemeResource();
                Main.loadTheme();
            },

            'check_cloexec_fds': () => {
                Shell.util_check_cloexec_fds();
            },
        };

        let title = _('Run a Command');

        let content = new Dialog.MessageDialogContent({title});
        this.contentLayout.add_child(content);

        let entry = new St.Entry({
            style_class: 'run-dialog-entry',
            can_focus: true,
        });
        ShellEntry.addContextMenu(entry);

        this._entryText = entry.clutter_text;
        content.add_child(entry);
        this.setInitialKeyFocus(this._entryText);

        let defaultDescriptionText = _('Press ESC to close');

        this._descriptionLabel = new St.Label({
            style_class: 'run-dialog-description',
            text: defaultDescriptionText,
        });
        content.add_child(this._descriptionLabel);

        this._commandError = false;

        this._pathCompleter = new Gio.FilenameCompleter();

        this._history = new History.HistoryManager({
            gsettingsKey: HISTORY_KEY,
            entry: this._entryText,
        });
        this._entryText.connect('activate', o => {
            this.popModal();
            this._run(o.get_text(),
                Clutter.get_current_event().get_state() & Clutter.ModifierType.CONTROL_MASK);
            if (!this._commandError ||
                !this.pushModal())
                this.close();
        });
        this._entryText.connect('key-press-event', (o, e) => {
            let symbol = e.get_key_symbol();
            if (symbol === Clutter.KEY_Tab) {
                let text = o.get_text();
                let prefix;
                if (text.lastIndexOf(' ') === -1)
                    prefix = text;
                else
                    prefix = text.substr(text.lastIndexOf(' ') + 1);
                let postfix = this._getCompletion(prefix);
                if (postfix != null && postfix.length > 0) {
                    o.insert_text(postfix, -1);
                    o.set_cursor_position(text.length + postfix.length);
                }
                return Clutter.EVENT_STOP;
            }
            return Clutter.EVENT_PROPAGATE;
        });
        this._entryText.connect('text-changed', () => {
            this._descriptionLabel.set_text(defaultDescriptionText);
        });
    }

    vfunc_key_release_event(event) {
        if (event.get_key_symbol() === Clutter.KEY_Escape) {
            this.close();
            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _getCommandCompletion(text) {
        function _getCommon(s1, s2) {
            if (s1 == null)
                return s2;

            let k = 0;
            for (; k < s1.length && k < s2.length; k++) {
                if (s1[k] !== s2[k])
                    break;
            }
            if (k === 0)
                return '';
            return s1.substr(0, k);
        }

        let paths = GLib.getenv('PATH').split(':');
        paths.push(GLib.get_home_dir());
        let someResults = paths.map(path => {
            let results = [];
            try {
                let file = Gio.File.new_for_path(path);
                let fileEnum = file.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
                let info;
                while ((info = fileEnum.next_file(null))) {
                    let name = info.get_name();
                    if (name.slice(0, text.length) === text)
                        results.push(name);
                }
            } catch (e) {
                if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND) &&
                    !e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_DIRECTORY))
                    log(e);
            }
            return results;
        });
        let results = someResults.reduce((a, b) => a.concat(b), []);

        if (!results.length)
            return null;

        let common = results.reduce(_getCommon, null);
        return common.substr(text.length);
    }

    _getCompletion(text) {
        if (text.includes('/'))
            return this._pathCompleter.get_completion_suffix(text);
        else
            return this._getCommandCompletion(text);
    }

    _run(input, inTerminal) {
        input = this._history.addItem(input); // trims input
        let command = input;

        this._commandError = false;
        let f;
        if (this._enableInternalCommands)
            f = this._internalCommands[input];
        else
            f = null;
        if (f) {
            f();
        } else {
            try {
                if (inTerminal) {
                    let exec = this._terminalSettings.get_string(EXEC_KEY);
                    let execArg = this._terminalSettings.get_string(EXEC_ARG_KEY);
                    command = `${exec} ${execArg} ${input}`;
                }
                Util.trySpawnCommandLine(command);
            } catch (e) {
                // Mmmh, that failed - see if @input matches an existing file
                let path = null;
                if (input.charAt(0) === '/') {
                    path = input;
                } else if (input) {
                    if (input.charAt(0) === '~')
                        input = input.slice(1);
                    path = `${GLib.get_home_dir()}/${input}`;
                }

                if (path && GLib.file_test(path, GLib.FileTest.EXISTS)) {
                    let file = Gio.file_new_for_path(path);
                    try {
                        Gio.app_info_launch_default_for_uri(file.get_uri(),
                            global.create_app_launch_context(0, -1));
                    } catch (err) {
                        // The exception from gjs contains an error string like:
                        //     Error invoking Gio.app_info_launch_default_for_uri: No application
                        //     is registered as handling this file
                        // We are only interested in the part after the first colon.
                        let message = err.message.replace(/[^:]*: *(.+)/, '$1');
                        this._showError(message);
                    }
                } else {
                    this._showError(e.message);
                }
            }
        }
    }

    _showError(message) {
        this._commandError = true;
        this._descriptionLabel.set_text(message);
    }

    _restart() {
        if (Meta.is_wayland_compositor()) {
            this._showError(_('Restart is not available on Wayland'));
            return;
        }
        this._shouldFadeOut = false;
        this.close();
        Meta.restart(_('Restarting…'), global.context);
    }

    open() {
        this._history.lastItem();
        this._entryText.set_text('');
        this._commandError = false;

        if (this._lockdownSettings.get_boolean(DISABLE_COMMAND_LINE_KEY))
            return false;

        return super.open();
    }
});
(uuay)authList.js{// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/*
 * Copyright 2017 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import St from 'gi://St';

const SCROLL_ANIMATION_TIME = 500;

const AuthListItem = GObject.registerClass({
    Signals: {'activate': {}},
}, class AuthListItem extends St.Button {
    _init(key, text) {
        this.key = key;
        const label = new St.Label({
            text,
            style_class: 'login-dialog-auth-list-label',
            y_align: Clutter.ActorAlign.CENTER,
            x_expand: false,
        });

        super._init({
            style_class: 'login-dialog-auth-list-item',
            button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
            can_focus: true,
            child: label,
            reactive: true,
        });

        this.connect('key-focus-in',
            () => this._setSelected(true));
        this.connect('key-focus-out',
            () => this._setSelected(false));
        this.connect('notify::hover',
            () => this._setSelected(this.hover));

        this.connect('clicked', this._onClicked.bind(this));
    }

    _onClicked() {
        this.emit('activate');
    }

    _setSelected(selected) {
        if (selected) {
            this.add_style_pseudo_class('selected');
            this.grab_key_focus();
        } else {
            this.remove_style_pseudo_class('selected');
        }
    }
});

export const AuthList = GObject.registerClass({
    Signals: {
        'activate': {param_types: [GObject.TYPE_STRING]},
        'item-added': {param_types: [AuthListItem.$gtype]},
    },
}, class AuthList extends St.BoxLayout {
    _init() {
        super._init({
            vertical: true,
            style_class: 'login-dialog-auth-list-layout',
            x_align: Clutter.ActorAlign.START,
            y_align: Clutter.ActorAlign.CENTER,
        });

        this.label = new St.Label({style_class: 'login-dialog-auth-list-title'});
        this.add_child(this.label);

        this._box = new St.BoxLayout({
            vertical: true,
            style_class: 'login-dialog-auth-list',
            pseudo_class: 'expanded',
        });

        this._scrollView = new St.ScrollView({
            style_class: 'login-dialog-auth-list-view',
            child: this._box,
        });
        this.add_child(this._scrollView);

        this._items = new Map();

        this.connect('key-focus-in', this._moveFocusToItems.bind(this));
    }

    _moveFocusToItems() {
        let hasItems = this.numItems > 0;

        if (!hasItems)
            return;

        if (global.stage.get_key_focus() !== this)
            return;

        let focusSet = this.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
        if (!focusSet) {
            const laters = global.compositor.get_laters();
            laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                this._moveFocusToItems();
                return false;
            });
        }
    }

    _onItemActivated(activatedItem) {
        this.emit('activate', activatedItem.key);
    }

    scrollToItem(item) {
        let box = item.get_allocation_box();

        const adjustment = this._scrollView.vadjustment;

        let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0);
        adjustment.ease(value, {
            duration: SCROLL_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    addItem(key, text) {
        this.removeItem(key);

        let item = new AuthListItem(key, text);
        this._box.add_child(item);

        this._items.set(key, item);

        item.connect('activate', this._onItemActivated.bind(this));

        // Try to keep the focused item front-and-center
        item.connect('key-focus-in', () => this.scrollToItem(item));

        this._moveFocusToItems();

        this.emit('item-added', item);
    }

    removeItem(key) {
        if (!this._items.has(key))
            return;

        let item = this._items.get(key);

        item.destroy();

        this._items.delete(key);
    }

    get numItems() {
        return this._items.size;
    }

    clear() {
        this.label.text = '';
        this._box.destroy_all_children();
        this._items.clear();
    }
});
(uuay)webLogin.js!// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
//
// A widget showing a URL for web login
/* exported WebLoginPrompt */

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Shell from 'gi://Shell';
import Pango from 'gi://Pango';
import St from 'gi://St';
import {Spinner} from '../ui/animation.js';
import * as ModalDialog from '../ui/modalDialog.js';
import * as Params from '../misc/params.js';

const QR_CODE_SIZE = 150;
const WEB_LOGIN_SPINNER_SIZE = 35;

Gio._promisify(Shell.QrCodeGenerator.prototype, 'generate_qr_code');

const QrCode = GObject.registerClass(
class QrCode extends St.Bin {
    _init(params) {
        const themeContext = St.ThemeContext.get_for_stage(global.stage);
        const {iconSize, url} = Params.parse(params, {
            iconSize: QR_CODE_SIZE,
            url: null,
        });

        super._init({
            width: QR_CODE_SIZE,
            height: QR_CODE_SIZE,
            x_align: Clutter.ActorAlign.CENTER,
        });

        this._iconSize = iconSize;
        this._url = url;
        this.child = new St.Icon({
            icon_size: this._iconSize,
            style_class: 'qr-code',
        });

        this.connect('destroy', () => this._cancellable?.cancel());

        themeContext.connectObject('notify::scale-factor',
            () => this.update().catch(logError), this);
    }

    vfunc_style_changed() {
        super.vfunc_style_changed();

        let changed = false;
        const node = this.child.get_theme_node();
        const [found, iconSize] = node.lookup_length('icon-size', false);

        if (found) {
            const themeContext = St.ThemeContext.get_for_stage(global.stage);
            const newIconSize = iconSize / themeContext.scaleFactor;
            if (this._iconSize !== newIconSize) {
                this._iconSize = newIconSize;
                changed = true;
            }
        }

        const bgColor = node.get_background_color();
        const fgColor = node.get_foreground_color();

        if (!this._bgColor?.equal(bgColor)) {
            this._bgColor = bgColor;
            changed = true;
        }

        if (!this._fgColor?.equal(fgColor)) {
            this._fgColor = fgColor;
            changed = true;
        }

        if (!changed)
            return;

        this.update().catch(logError);
    }

    async update() {
        let {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
        this.set_size(
            this._iconSize * scaleFactor,
            this._iconSize * scaleFactor);

        this._cancellable?.cancel();
        this._cancellable = new Gio.Cancellable();
        const qrCodeGenerator = new Shell.QrCodeGenerator();

        try {
            this.child.gicon = await qrCodeGenerator.generate_qr_code(
                this._url, this._iconSize, this._iconSize,
                this._bgColor, this._fgColor, this._cancellable);
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                logError(e);
        }
    }
});

export const WebLoginPrompt = GObject.registerClass(
class WebLoginPrompt extends St.BoxLayout {
    _init(params) {
        const {iconSize, message, url, code} = Params.parse(params, {
            iconSize: QR_CODE_SIZE,
            message: null,
            url: null,
            code: null,
        });

        super._init({
            styleClass: 'web-login-prompt',
            vertical: true,
        });

        this._urlTitleLabel = new St.Label({
            text: message,
            style_class: 'web-login-title-label',
        });
        this._urlTitleLabel.clutterText.set({
            lineWrap: true,
            ellipsize: Pango.EllipsizeMode.NONE,
        });
        this.add_child(this._urlTitleLabel);

        this._qrCode = new QrCode({iconSize, url});
        this.add_child(this._qrCode);

        this._urlLabel = new St.Label({
            style_class: 'web-login-url-label',
            text: this._formatURLForDisplay(url),
            x_expand: true,
        });
        this.add_child(this._urlLabel);

        if (code) {
            this._codeBox = new St.BoxLayout({
                x_align: Clutter.ActorAlign.CENTER,
                x_expand: true,
            });
            this.add_child(this._codeBox);

            this._codeTitleLabel = new St.Label({
                text: _('Login code: '),
                style_class: 'web-login-code-title-label',
            });
            this._codeBox.add_child(this._codeTitleLabel);

            this._codeLabel = new St.Label({
                text: code,
                style_class: 'web-login-code-label',
            });
            this._codeBox.add_child(this._codeLabel);
        }
    }

    _formatURLForDisplay(url) {
        const http = 'http://';
        const https = 'https://';

        if (url.startsWith(http))
            return url.substring(http.length);

        if (url.startsWith(https))
            return url.substring(https.length);

        return url;
    }
});

export const WebLoginDialog = GObject.registerClass({
    Signals: {
        'cancel': {},
        'done': {},
    },
}, class WebLoginDialog extends ModalDialog.ModalDialog {
    _init(params) {
        const {message, url, code} = Params.parse(params, {
            message: null,
            url: null,
            code: null,
        });

        super._init({
            shouldFadeOut: false,
            styleClass: 'web-login-dialog',
        });

        this._webLoginPrompt = new WebLoginPrompt({code, message, url});
        this._webLoginPrompt.set({
            y_align: Clutter.ActorAlign.CENTER,
        });

        this.contentLayout.reactive = false;
        this.contentLayout.can_focus = false;
        this.contentLayout.add_child(this._webLoginPrompt);
        this._updateButtons();

        this._contentOverlay = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            style_class: 'web-login-dialog-content-overlay',
        });
        this._contentOverlay.hide();

        this.backgroundStack.add_child(this._contentOverlay);

        const constraint = new Clutter.BindConstraint({
            source: this.dialogLayout,
            coordinate: Clutter.BindCoordinate.ALL,
        });

        this._contentOverlay.add_constraint(constraint);

        this._spinnerFrame = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            styleClass: 'web-login-spinner',
        });
        this._contentOverlay.add_child(this._spinnerFrame);

        this._spinner = new Spinner(WEB_LOGIN_SPINNER_SIZE, {
            hideOnStop: true,
        });
        this._spinnerFrame.add_child(this._spinner);
    }

    done() {
        this._doneButton.reactive = false;
        this._doneButton.can_focus = false;

        this._contentOverlay.show();
        global.stage.set_key_focus(this.dialogLayout);
        this._spinner.play();

        this.emit('done');
    }

    _updateButtons() {
        this.clearButtons();

        this._cancelButton = this.addButton({
            action: this.cancel.bind(this),
            label: _('Cancel'),
            key: Clutter.KEY_Escape,
        });

        this._doneButton = this.addButton({
            action: this.done.bind(this),
            default: true,
            label: _('Done'),
        });
    }

    cancel() {
        this.emit('cancel');
        this.close();
    }
});

export var WebLoginIntro = GObject.registerClass(
class WebLoginIntro extends St.Button {
    _init(params) {
        const {message} = Params.parse(params, {
            message: null,
        });

        const label = new St.Label({
            text: message,
            style_class: 'web-login-intro-button-label',
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
            x_expand: true,
            y_expand: false,
        });

        label.clutter_text.line_wrap = true;
        label.clutter_text.y_align = Clutter.ActorAlign.CENTER;
        label.clutter_text.x_align = Clutter.ActorAlign.CENTER;

        super._init({
            style_class: 'web-login-prompt login-dialog-button web-login-intro-button',
            accessible_name: message,
            button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
            reactive: true,
            can_focus: true,
            child: label,
        });
    }
});
(uuay)switcherPopup.js�T// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St';

import * as Main from './main.js';

const POPUP_DELAY_TIMEOUT = 150; // milliseconds

const POPUP_SCROLL_TIME = 100; // milliseconds
const POPUP_FADE_OUT_TIME = 100; // milliseconds

const DISABLE_HOVER_TIMEOUT = 500; // milliseconds
const NO_MODS_TIMEOUT = 1500; // milliseconds

/**
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
export function mod(a, b) {
    return (a + b) % b;
}

function primaryModifier(mask) {
    if (mask === 0)
        return 0;

    let primary = 1;
    while (mask > 1) {
        mask >>= 1;
        primary <<= 1;
    }
    return primary;
}

export const SwitcherPopup = GObject.registerClass({
    GTypeFlags: GObject.TypeFlags.ABSTRACT,
}, class SwitcherPopup extends St.Widget {
    _init(items) {
        super._init({
            style_class: 'switcher-popup',
            reactive: true,
            visible: false,
        });

        this._switcherList = null;

        this._items = items || [];
        this._selectedIndex = 0;

        this.connect('destroy', this._onDestroy.bind(this));

        Main.uiGroup.add_child(this);

        Main.layoutManager.connectObject(
            'system-modal-opened', () => this.destroy(), this);

        this._haveModal = false;
        this._modifierMask = 0;

        this._motionTimeoutId = 0;
        this._initialDelayTimeoutId = 0;
        this._noModsTimeoutId = 0;

        this.add_constraint(new Clutter.BindConstraint({
            source: global.stage,
            coordinate: Clutter.BindCoordinate.ALL,
        }));

        // Initially disable hover so we ignore the enter-event if
        // the switcher appears underneath the current pointer location
        this._disableHover();
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        let childBox = new Clutter.ActorBox();
        let primary = Main.layoutManager.primaryMonitor;

        let leftPadding = this.get_theme_node().get_padding(St.Side.LEFT);
        let rightPadding = this.get_theme_node().get_padding(St.Side.RIGHT);
        let hPadding = leftPadding + rightPadding;

        // Allocate the switcherList
        // We select a size based on an icon size that does not overflow the screen
        let [, childNaturalHeight] = this._switcherList.get_preferred_height(primary.width - hPadding);
        let [, childNaturalWidth] = this._switcherList.get_preferred_width(childNaturalHeight);
        childBox.x1 = Math.max(primary.x + leftPadding, primary.x + Math.floor((primary.width - childNaturalWidth) / 2));
        childBox.x2 = Math.min(primary.x + primary.width - rightPadding, childBox.x1 + childNaturalWidth);
        childBox.y1 = primary.y + Math.floor((primary.height - childNaturalHeight) / 2);
        childBox.y2 = childBox.y1 + childNaturalHeight;
        this._switcherList.allocate(childBox);
    }

    _initialSelection(backward, _binding) {
        if (backward)
            this._select(this._items.length - 1);
        else if (this._items.length === 1)
            this._select(0);
        else
            this._select(1);
    }

    show(backward, binding, mask) {
        if (this._items.length === 0)
            return false;

        let grab = Main.pushModal(this);
        // We expect at least a keyboard grab here
        if ((grab.get_seat_state() & Clutter.GrabState.KEYBOARD) === 0) {
            Main.popModal(grab);
            return false;
        }
        this._grab = grab;
        this._haveModal = true;
        this._modifierMask = primaryModifier(mask);

        this.add_child(this._switcherList);
        this._switcherList.connect('item-activated', this._itemActivated.bind(this));
        this._switcherList.connect('item-entered', this._itemEntered.bind(this));
        this._switcherList.connect('item-removed', this._itemRemoved.bind(this));

        // Need to force an allocation so we can figure out whether we
        // need to scroll when selecting
        this.opacity = 0;
        this.visible = true;
        this.get_allocation_box();

        this._initialSelection(backward, binding);

        // There's a race condition; if the user released Alt before
        // we got the grab, then we won't be notified. (See
        // https://bugzilla.gnome.org/show_bug.cgi?id=596695 for
        // details.) So we check now. (Have to do this after updating
        // selection.)
        if (this._modifierMask) {
            let [x_, y_, mods] = global.get_pointer();
            if (!(mods & this._modifierMask)) {
                this._finish(global.get_current_time());
                return true;
            }
        } else {
            this._resetNoModsTimeout();
        }

        // We delay showing the popup so that fast Alt+Tab users aren't
        // disturbed by the popup briefly flashing.
        this._initialDelayTimeoutId = GLib.timeout_add(
            GLib.PRIORITY_DEFAULT,
            POPUP_DELAY_TIMEOUT,
            () => {
                this._showImmediately();
                return GLib.SOURCE_REMOVE;
            });
        GLib.Source.set_name_by_id(this._initialDelayTimeoutId, '[gnome-shell] Main.osdWindow.cancel');
        return true;
    }

    _showImmediately() {
        if (this._initialDelayTimeoutId === 0)
            return;

        GLib.source_remove(this._initialDelayTimeoutId);
        this._initialDelayTimeoutId = 0;

        Main.osdWindowManager.hideAll();
        this.opacity = 255;
    }

    _next() {
        return mod(this._selectedIndex + 1, this._items.length);
    }

    _previous() {
        return mod(this._selectedIndex - 1, this._items.length);
    }

    _keyPressHandler(_keysym, _action) {
        throw new GObject.NotImplementedError(`_keyPressHandler in ${this.constructor.name}`);
    }

    vfunc_key_press_event(event) {
        let keysym = event.get_key_symbol();
        let action = global.display.get_keybinding_action(
            event.get_key_code(), event.get_state());

        this._disableHover();

        if (this._keyPressHandler(keysym, action) !== Clutter.EVENT_PROPAGATE) {
            this._showImmediately();
            return Clutter.EVENT_STOP;
        }

        // Note: pressing one of the below keys will destroy the popup only if
        // that key is not used by the active popup's keyboard shortcut
        if (keysym === Clutter.KEY_Escape || keysym === Clutter.KEY_Tab)
            this.fadeAndDestroy();

        // Allow to explicitly select the current item; this is particularly
        // useful for no-modifier popups
        if (keysym === Clutter.KEY_space ||
            keysym === Clutter.KEY_Return ||
            keysym === Clutter.KEY_KP_Enter ||
            keysym === Clutter.KEY_ISO_Enter)
            this._finish(event.get_time());

        return Clutter.EVENT_STOP;
    }

    vfunc_key_release_event(event) {
        if (this._modifierMask) {
            let [x_, y_, mods] = global.get_pointer();
            let state = mods & this._modifierMask;

            if (state === 0)
                this._finish(event.get_time());
        } else {
            this._resetNoModsTimeout();
        }

        return Clutter.EVENT_STOP;
    }

    vfunc_button_press_event() {
        /* We clicked outside */
        this.fadeAndDestroy();
        return Clutter.EVENT_PROPAGATE;
    }

    _scrollHandler(direction) {
        if (direction === Clutter.ScrollDirection.UP)
            this._select(this._previous());
        else if (direction === Clutter.ScrollDirection.DOWN)
            this._select(this._next());
    }

    vfunc_scroll_event(event) {
        this._disableHover();

        this._scrollHandler(event.get_scroll_direction());
        return Clutter.EVENT_PROPAGATE;
    }

    _itemActivatedHandler(n) {
        this._select(n);
    }

    _itemActivated(switcher, n) {
        this._itemActivatedHandler(n);
        this._finish(global.get_current_time());
    }

    _itemEnteredHandler(n) {
        this._select(n);
    }

    _itemEntered(switcher, n) {
        if (!this.mouseActive)
            return;
        this._itemEnteredHandler(n);
    }

    _itemRemovedHandler(n) {
        if (this._items.length > 0) {
            let newIndex;

            if (n < this._selectedIndex)
                newIndex = this._selectedIndex - 1;
            else if (n === this._selectedIndex)
                newIndex = Math.min(n, this._items.length - 1);
            else if (n > this._selectedIndex)
                return; // No need to select something new in this case

            this._select(newIndex);
        } else {
            this.fadeAndDestroy();
        }
    }

    _itemRemoved(switcher, n) {
        this._itemRemovedHandler(n);
    }

    _disableHover() {
        this.mouseActive = false;

        if (this._motionTimeoutId !== 0)
            GLib.source_remove(this._motionTimeoutId);

        this._motionTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DISABLE_HOVER_TIMEOUT, this._mouseTimedOut.bind(this));
        GLib.Source.set_name_by_id(this._motionTimeoutId, '[gnome-shell] this._mouseTimedOut');
    }

    _mouseTimedOut() {
        this._motionTimeoutId = 0;
        this.mouseActive = true;
        return GLib.SOURCE_REMOVE;
    }

    _resetNoModsTimeout() {
        if (this._noModsTimeoutId !== 0)
            GLib.source_remove(this._noModsTimeoutId);

        this._noModsTimeoutId = GLib.timeout_add(
            GLib.PRIORITY_DEFAULT,
            NO_MODS_TIMEOUT,
            () => {
                this._finish(global.display.get_current_time_roundtrip());
                this._noModsTimeoutId = 0;
                return GLib.SOURCE_REMOVE;
            });
    }

    _popModal() {
        if (this._haveModal) {
            Main.popModal(this._grab);
            this._grab = null;
            this._haveModal = false;
        }
    }

    fadeAndDestroy() {
        this._popModal();
        if (this.opacity > 0) {
            this.ease({
                opacity: 0,
                duration: POPUP_FADE_OUT_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => this.destroy(),
            });
        } else {
            this.destroy();
        }
    }

    _finish(_timestamp) {
        this.fadeAndDestroy();
    }

    _onDestroy() {
        this._popModal();

        if (this._motionTimeoutId !== 0)
            GLib.source_remove(this._motionTimeoutId);
        if (this._initialDelayTimeoutId !== 0)
            GLib.source_remove(this._initialDelayTimeoutId);
        if (this._noModsTimeoutId !== 0)
            GLib.source_remove(this._noModsTimeoutId);

        // Make sure the SwitcherList is always destroyed, it may not be
        // a child of the actor at this point.
        if (this._switcherList)
            this._switcherList.destroy();
    }

    _select(num) {
        this._selectedIndex = num;
        this._switcherList.highlight(num);
    }
});

const SwitcherButton = GObject.registerClass(
class SwitcherButton extends St.Button {
    _init(square) {
        super._init({
            style_class: 'item-box',
            reactive: true,
        });

        this._square = square;
    }

    vfunc_get_preferred_width(forHeight) {
        if (this._square)
            return this.get_preferred_height(-1);
        else
            return super.vfunc_get_preferred_width(forHeight);
    }
});

export const SwitcherList = GObject.registerClass({
    Signals: {
        'item-activated': {param_types: [GObject.TYPE_INT]},
        'item-entered': {param_types: [GObject.TYPE_INT]},
        'item-removed': {param_types: [GObject.TYPE_INT]},
    },
}, class SwitcherList extends St.Widget {
    _init(squareItems) {
        super._init({style_class: 'switcher-list'});

        this._list = new St.BoxLayout({
            style_class: 'switcher-list-item-container',
            vertical: false,
            x_expand: true,
            y_expand: true,
        });

        let layoutManager = this._list.get_layout_manager();

        this._list.spacing = 0;
        this._list.connect('style-changed', () => {
            this._list.spacing = this._list.get_theme_node().get_length('spacing');
        });

        this._scrollView = new St.ScrollView({
            style_class: 'hfade',
            enable_mouse_scrolling: false,
            hscrollbar_policy: St.PolicyType.NEVER,
            vscrollbar_policy: St.PolicyType.NEVER,
            child: this._list,
        });

        this.add_child(this._scrollView);

        // Those arrows indicate whether scrolling in one direction is possible
        this._leftArrow = new St.DrawingArea({
            style_class: 'switcher-arrow',
            pseudo_class: 'highlighted',
        });
        this._leftArrow.connect('repaint', () => {
            drawArrow(this._leftArrow, St.Side.LEFT);
        });
        this._rightArrow = new St.DrawingArea({
            style_class: 'switcher-arrow',
            pseudo_class: 'highlighted',
        });
        this._rightArrow.connect('repaint', () => {
            drawArrow(this._rightArrow, St.Side.RIGHT);
        });

        this.add_child(this._leftArrow);
        this.add_child(this._rightArrow);

        this._items = [];
        this._highlighted = -1;
        this._squareItems = squareItems;
        this._scrollableRight = true;
        this._scrollableLeft = false;

        layoutManager.homogeneous = squareItems;
    }

    addItem(item, label) {
        let bbox = new SwitcherButton(this._squareItems);

        bbox.set_child(item);
        this._list.add_child(bbox);

        bbox.connect('clicked', () => this._onItemClicked(bbox));
        bbox.connect('motion-event', () => this._onItemMotion(bbox));

        bbox.label_actor = label;

        this._items.push(bbox);

        return bbox;
    }

    removeItem(index) {
        let item = this._items.splice(index, 1);
        item[0].destroy();
        this.emit('item-removed', index);
    }

    addAccessibleState(index, state) {
        this._items[index].add_accessible_state(state);
    }

    removeAccessibleState(index, state) {
        this._items[index].remove_accessible_state(state);
    }

    _onItemClicked(item) {
        this._itemActivated(this._items.indexOf(item));
    }

    _onItemMotion(item) {
        // Avoid reentrancy
        if (item !== this._items[this._highlighted])
            this._itemEntered(this._items.indexOf(item));

        return Clutter.EVENT_PROPAGATE;
    }

    highlight(index, justOutline) {
        if (this._items[this._highlighted]) {
            this._items[this._highlighted].remove_style_pseudo_class('selected');
            this._items[this._highlighted].remove_style_pseudo_class('highlighted');
        }

        if (this._items[index]) {
            if (justOutline)
                this._items[index].add_style_pseudo_class('highlighted');
            else
                this._items[index].add_style_pseudo_class('selected');
        }

        this._highlighted = index;

        const adjustment = this._scrollView.hadjustment;
        let [value] = adjustment.get_values();
        let [absItemX] = this._items[index].get_transformed_position();
        let [result_, posX, posY_] = this.transform_stage_point(absItemX, 0);
        let [containerWidth] = this.get_transformed_size();
        if (posX + this._items[index].get_width() > containerWidth)
            this._scrollToRight(index);
        else if (this._items[index].allocation.x1 - value < 0)
            this._scrollToLeft(index);
    }

    _scrollToLeft(index) {
        const adjustment = this._scrollView.hadjustment;
        let [value, lower_, upper, stepIncrement_, pageIncrement_, pageSize] = adjustment.get_values();

        let item = this._items[index];

        if (item.allocation.x1 < value)
            value = Math.max(0, item.allocation.x1);
        else if (item.allocation.x2 > value + pageSize)
            value = Math.min(upper, item.allocation.x2 - pageSize);

        this._scrollableRight = true;
        adjustment.ease(value, {
            progress_mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            duration: POPUP_SCROLL_TIME,
            onComplete: () => {
                if (index === 0)
                    this._scrollableLeft = false;
                this.queue_relayout();
            },
        });
    }

    _scrollToRight(index) {
        const adjustment = this._scrollView.hadjustment;
        let [value, lower_, upper, stepIncrement_, pageIncrement_, pageSize] = adjustment.get_values();

        let item = this._items[index];

        if (item.allocation.x1 < value)
            value = Math.max(0, item.allocation.x1);
        else if (item.allocation.x2 > value + pageSize)
            value = Math.min(upper, item.allocation.x2 - pageSize);

        this._scrollableLeft = true;
        adjustment.ease(value, {
            progress_mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            duration: POPUP_SCROLL_TIME,
            onComplete: () => {
                if (index === this._items.length - 1)
                    this._scrollableRight = false;
                this.queue_relayout();
            },
        });
    }

    _itemActivated(n) {
        this.emit('item-activated', n);
    }

    _itemEntered(n) {
        this.emit('item-entered', n);
    }

    _maxChildWidth(forHeight) {
        let maxChildMin = 0;
        let maxChildNat = 0;

        for (let i = 0; i < this._items.length; i++) {
            let [childMin, childNat] = this._items[i].get_preferred_width(forHeight);
            maxChildMin = Math.max(childMin, maxChildMin);
            maxChildNat = Math.max(childNat, maxChildNat);

            if (this._squareItems) {
                [childMin, childNat] = this._items[i].get_preferred_height(-1);
                maxChildMin = Math.max(childMin, maxChildMin);
                maxChildNat = Math.max(childNat, maxChildNat);
            }
        }

        return [maxChildMin, maxChildNat];
    }

    vfunc_get_preferred_width(forHeight) {
        let themeNode = this.get_theme_node();
        let [maxChildMin] = this._maxChildWidth(forHeight);
        let [minListWidth] = this._list.get_preferred_width(forHeight);

        return themeNode.adjust_preferred_width(maxChildMin, minListWidth);
    }

    vfunc_get_preferred_height(_forWidth) {
        let maxChildMin = 0;
        let maxChildNat = 0;

        for (let i = 0; i < this._items.length; i++) {
            let [childMin, childNat] = this._items[i].get_preferred_height(-1);
            maxChildMin = Math.max(childMin, maxChildMin);
            maxChildNat = Math.max(childNat, maxChildNat);
        }

        if (this._squareItems) {
            let [childMin] = this._maxChildWidth(-1);
            maxChildMin = Math.max(childMin, maxChildMin);
            maxChildNat = maxChildMin;
        }

        let themeNode = this.get_theme_node();
        return themeNode.adjust_preferred_height(maxChildMin, maxChildNat);
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        let contentBox = this.get_theme_node().get_content_box(box);
        let width = contentBox.x2 - contentBox.x1;
        let height = contentBox.y2 - contentBox.y1;

        let leftPadding = this.get_theme_node().get_padding(St.Side.LEFT);
        let rightPadding = this.get_theme_node().get_padding(St.Side.RIGHT);

        let [minListWidth] = this._list.get_preferred_width(height);

        let childBox = new Clutter.ActorBox();
        let scrollable = minListWidth > width;

        this._scrollView.allocate(contentBox);

        let arrowWidth = Math.floor(leftPadding / 3);
        let arrowHeight = arrowWidth * 2;
        childBox.x1 = leftPadding / 2;
        childBox.y1 = this.height / 2 - arrowWidth;
        childBox.x2 = childBox.x1 + arrowWidth;
        childBox.y2 = childBox.y1 + arrowHeight;
        this._leftArrow.allocate(childBox);
        this._leftArrow.opacity = this._scrollableLeft && scrollable ? 255 : 0;

        arrowWidth = Math.floor(rightPadding / 3);
        arrowHeight = arrowWidth * 2;
        childBox.x1 = this.width - arrowWidth - rightPadding / 2;
        childBox.y1 = this.height / 2 - arrowWidth;
        childBox.x2 = childBox.x1 + arrowWidth;
        childBox.y2 = childBox.y1 + arrowHeight;
        this._rightArrow.allocate(childBox);
        this._rightArrow.opacity = this._scrollableRight && scrollable ? 255 : 0;
    }
});

/**
 * @param {St.DrawingArrow} area
 * @param {St.Side} side
 */
export function drawArrow(area, side) {
    let themeNode = area.get_theme_node();
    let borderColor = themeNode.get_border_color(side);
    let bodyColor = themeNode.get_foreground_color();

    let [width, height] = area.get_surface_size();
    let cr = area.get_context();

    cr.setLineWidth(1.0);
    cr.setSourceColor(borderColor);

    switch (side) {
    case St.Side.TOP:
        cr.moveTo(0, height);
        cr.lineTo(Math.floor(width * 0.5), 0);
        cr.lineTo(width, height);
        break;

    case St.Side.BOTTOM:
        cr.moveTo(width, 0);
        cr.lineTo(Math.floor(width * 0.5), height);
        cr.lineTo(0, 0);
        break;

    case St.Side.LEFT:
        cr.moveTo(width, height);
        cr.lineTo(0, Math.floor(height * 0.5));
        cr.lineTo(width, 0);
        break;

    case St.Side.RIGHT:
        cr.moveTo(0, 0);
        cr.lineTo(width, Math.floor(height * 0.5));
        cr.lineTo(0, height);
        break;
    }

    cr.strokePreserve();

    cr.setSourceColor(bodyColor);
    cr.fill();
    cr.$dispose();
}

(uuay)userWidget.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
//
// A widget showing the user avatar and name

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St';

import * as Params from '../misc/params.js';

const AVATAR_ICON_SIZE = 64;

// Adapted from gdm/gui/user-switch-applet/applet.c
//
// Copyright (C) 2004-2005 James M. Cape <jcape@ignore-your.tv>.
// Copyright (C) 2008,2009 Red Hat, Inc.

export const Avatar = GObject.registerClass(
class Avatar extends St.Bin {
    _init(user, params) {
        let themeContext = St.ThemeContext.get_for_stage(global.stage);
        params = Params.parse(params, {
            styleClass: 'user-icon',
            reactive: false,
            iconSize: AVATAR_ICON_SIZE,
        });

        super._init({
            style_class: params.styleClass,
            reactive: params.reactive,
            width: params.iconSize * themeContext.scaleFactor,
            height: params.iconSize * themeContext.scaleFactor,
            x_expand: false,
            y_expand: false,
        });

        this._iconSize = params.iconSize;
        this._user = user;

        this.bind_property('reactive', this, 'track-hover',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('reactive', this, 'can-focus',
            GObject.BindingFlags.SYNC_CREATE);

        // Monitor the scaling factor to make sure we recreate the avatar when needed.
        themeContext.connectObject('notify::scale-factor', this.update.bind(this), this);
    }

    vfunc_style_changed() {
        super.vfunc_style_changed();

        let node = this.get_theme_node();
        let [found, iconSize] = node.lookup_length('icon-size', false);

        if (!found)
            return;

        let themeContext = St.ThemeContext.get_for_stage(global.stage);

        // node.lookup_length() returns a scaled value, but we
        // need unscaled
        this._iconSize = iconSize / themeContext.scaleFactor;
        this.update();
    }

    setSensitive(sensitive) {
        this.reactive = sensitive;
    }

    update() {
        let iconFile = null;
        if (this._user) {
            iconFile = this._user.get_icon_file();
            if (iconFile && !GLib.file_test(iconFile, GLib.FileTest.EXISTS))
                iconFile = null;
        }

        let {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
        this.set_size(
            this._iconSize * scaleFactor,
            this._iconSize * scaleFactor);

        if (iconFile) {
            this.child = null;
            this.add_style_class_name('user-avatar');
            this.style = `
                background-image: url("${iconFile}");
                background-size: cover;`;
        } else {
            this.style = null;
            this.child = new St.Icon({
                icon_name: 'avatar-default-symbolic',
                icon_size: this._iconSize,
                x_expand: true,
                y_expand: true,
            });
        }
    }
});

export const UserWidgetLabel = GObject.registerClass(
class UserWidgetLabel extends St.Widget {
    _init(user) {
        super._init({layout_manager: new Clutter.BinLayout()});

        this._user = user;

        this._realNameLabel = new St.Label({
            style_class: 'user-widget-label',
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this._realNameLabel);

        this._userNameLabel = new St.Label({
            style_class: 'user-widget-label',
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this._userNameLabel);

        this._currentLabel = null;

        this._user.connectObject(
            'notify::is-loaded', this._updateUser.bind(this),
            'changed', this._updateUser.bind(this), this);
        this._updateUser();
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        let availWidth = box.x2 - box.x1;
        let availHeight = box.y2 - box.y1;

        let [, , natRealNameWidth] = this._realNameLabel.get_preferred_size();

        let childBox = new Clutter.ActorBox();

        let hiddenLabel;
        if (natRealNameWidth <= availWidth) {
            this._currentLabel = this._realNameLabel;
            hiddenLabel = this._userNameLabel;
        } else {
            this._currentLabel = this._userNameLabel;
            hiddenLabel = this._realNameLabel;
        }
        this.label_actor = this._currentLabel;

        hiddenLabel.allocate(childBox);

        childBox.set_size(availWidth, availHeight);

        this._currentLabel.allocate(childBox);
    }

    vfunc_paint(paintContext) {
        this._currentLabel.paint(paintContext);
    }

    _updateUser() {
        if (this._user.is_loaded) {
            this._realNameLabel.text = this._user.get_real_name();
            this._userNameLabel.text = this._user.get_user_name();
        } else {
            this._realNameLabel.text = '';
            this._userNameLabel.text = '';
        }
    }
});

export const UserWidget = GObject.registerClass(
class UserWidget extends St.BoxLayout {
    _init(user, orientation = Clutter.Orientation.HORIZONTAL) {
        // If user is null, that implies a username-based login authorization.
        this._user = user;

        let vertical = orientation === Clutter.Orientation.VERTICAL;
        let xAlign = vertical ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START;
        let styleClass = vertical ? 'user-widget vertical' : 'user-widget horizontal';

        super._init({
            styleClass,
            vertical,
            xAlign,
        });

        this._avatar = new Avatar(user);
        this._avatar.x_align = Clutter.ActorAlign.CENTER;
        this.add_child(this._avatar);

        this._userLoadedId = 0;
        this._userChangedId = 0;
        if (user) {
            this._label = new UserWidgetLabel(user);
            this.add_child(this._label);

            this._label.bind_property('label-actor',
                this, 'label-actor',
                GObject.BindingFlags.SYNC_CREATE);

            this._user.connectObject(
                'notify::is-loaded', this._updateUser.bind(this),
                'changed', this._updateUser.bind(this), this);
        } else {
            this._label = new St.Label({
                style_class: 'user-widget-label',
                text: 'Empty User',
                opacity: 0,
            });
            this.add_child(this._label);
        }

        this._updateUser();
    }

    _updateUser() {
        this._avatar.update();
    }
});
(uuay)nightLight.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';

import {QuickToggle, SystemIndicator} from '../quickSettings.js';

import {loadInterfaceXML} from '../../misc/fileUtils.js';

const BUS_NAME = 'org.gnome.SettingsDaemon.Color';
const OBJECT_PATH = '/org/gnome/SettingsDaemon/Color';

const ColorInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Color');
const colorInfo = Gio.DBusInterfaceInfo.new_for_xml(ColorInterface);

const NightLightToggle = GObject.registerClass(
class NightLightToggle extends QuickToggle {
    _init() {
        super._init({
            title: _('Night Light'),
            iconName: 'night-light-symbolic',
            toggleMode: true,
        });

        const monitorManager = global.backend.get_monitor_manager();
        monitorManager.bind_property('night-light-supported',
            this, 'visible',
            GObject.BindingFlags.SYNC_CREATE);

        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.settings-daemon.plugins.color',
        });
        this._settings.bind('night-light-enabled',
            this, 'checked',
            Gio.SettingsBindFlags.DEFAULT);
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'night-light-symbolic';

        this.quickSettingsItems.push(new NightLightToggle());

        this._proxy = new Gio.DBusProxy({
            g_connection: Gio.DBus.session,
            g_name: BUS_NAME,
            g_object_path: OBJECT_PATH,
            g_interface_name: colorInfo.name,
            g_interface_info: colorInfo,
        });
        this._proxy.connect('g-properties-changed', (p, properties) => {
            const nightLightActiveChanged = !!properties.lookup_value('NightLightActive', null);
            if (nightLightActiveChanged)
                this._sync();
        });
        this._proxy.init_async(GLib.PRIORITY_DEFAULT, null)
            .catch(e => console.error(e.message));

        this._sync();
    }

    _sync() {
        this._indicator.visible = this._proxy.NightLightActive;
    }
});
(uuay)org/�networkAgent.js�x// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import NM from 'gi://NM';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../../misc/signals.js';

import * as Dialog from '../dialog.js';
import * as MessageTray from '../messageTray.js';
import * as ModalDialog from '../modalDialog.js';
import * as ShellEntry from '../shellEntry.js';

Gio._promisify(Shell.NetworkAgent.prototype, 'init_async');
Gio._promisify(Shell.NetworkAgent.prototype, 'search_vpn_plugin');

const VPN_UI_GROUP = 'VPN Plugin UI';

const NetworkSecretDialog = GObject.registerClass(
class NetworkSecretDialog extends ModalDialog.ModalDialog {
    _init(agent, requestId, connection, settingName, hints, flags, contentOverride) {
        super._init({styleClass: 'prompt-dialog'});

        this._agent = agent;
        this._requestId = requestId;
        this._connection = connection;
        this._settingName = settingName;
        this._hints = hints;

        if (contentOverride)
            this._content = contentOverride;
        else
            this._content = this._getContent();

        let contentBox = new Dialog.MessageDialogContent({
            title: this._content.title,
            description: this._content.message,
        });

        let initialFocusSet = false;
        for (let i = 0; i < this._content.secrets.length; i++) {
            let secret = this._content.secrets[i];
            let reactive = secret.key != null;

            let entryParams = {
                style_class: 'prompt-dialog-password-entry',
                hint_text: secret.label,
                text: secret.value,
                can_focus: reactive,
                reactive,
                x_align: Clutter.ActorAlign.CENTER,
            };
            if (secret.password)
                secret.entry = new St.PasswordEntry(entryParams);
            else
                secret.entry = new St.Entry(entryParams);
            ShellEntry.addContextMenu(secret.entry);
            contentBox.add_child(secret.entry);

            if (secret.validate)
                secret.valid = secret.validate(secret);
            else // no special validation, just ensure it's not empty
                secret.valid = secret.value.length > 0;

            if (reactive) {
                if (!initialFocusSet) {
                    this.setInitialKeyFocus(secret.entry);
                    initialFocusSet = true;
                }

                secret.entry.clutter_text.connect('activate', this._onOk.bind(this));
                secret.entry.clutter_text.connect('text-changed', () => {
                    secret.value = secret.entry.get_text();
                    if (secret.validate)
                        secret.valid = secret.validate(secret);
                    else
                        secret.valid = secret.value.length > 0;
                    this._updateOkButton();
                });
            } else {
                secret.valid = true;
            }
        }

        if (this._content.secrets.some(s => s.password)) {
            let capsLockWarning = new ShellEntry.CapsLockWarning();
            contentBox.add_child(capsLockWarning);
        }

        if (flags & NM.SecretAgentGetSecretsFlags.WPS_PBC_ACTIVE) {
            let descriptionLabel = new St.Label({
                text: _('Alternatively you can connect by pushing the “WPS” button on your router.'),
                style_class: 'message-dialog-description',
            });
            descriptionLabel.clutter_text.line_wrap = true;
            descriptionLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;

            contentBox.add_child(descriptionLabel);
        }

        this.contentLayout.add_child(contentBox);

        this._okButton = {
            label: _('Connect'),
            action: this._onOk.bind(this),
            default: true,
        };

        this.setButtons([{
            label: _('Cancel'),
            action: this.cancel.bind(this),
            key: Clutter.KEY_Escape,
        }, this._okButton]);

        this._updateOkButton();
    }

    _updateOkButton() {
        let valid = true;
        for (let i = 0; i < this._content.secrets.length; i++) {
            let secret = this._content.secrets[i];
            valid &&= secret.valid;
        }

        this._okButton.button.reactive = valid;
        this._okButton.button.can_focus = valid;
    }

    _onOk() {
        let valid = true;
        for (let i = 0; i < this._content.secrets.length; i++) {
            let secret = this._content.secrets[i];
            valid &&= secret.valid;
            if (secret.key !== null) {
                if (this._settingName === 'vpn')
                    this._agent.add_vpn_secret(this._requestId, secret.key, secret.value);
                else
                    this._agent.set_password(this._requestId, secret.key, secret.value);
            }
        }

        if (valid) {
            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
            this.close(global.get_current_time());
        }
        // do nothing if not valid
    }

    cancel() {
        this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
        this.close(global.get_current_time());
    }

    _validateWpaPsk(secret) {
        let value = secret.value;
        if (value.length === 64) {
            // must be composed of hexadecimal digits only
            for (let i = 0; i < 64; i++) {
                if (!((value[i] >= 'a' && value[i] <= 'f') ||
                      (value[i] >= 'A' && value[i] <= 'F') ||
                      (value[i] >= '0' && value[i] <= '9')))
                    return false;
            }
            return true;
        }

        return value.length >= 8 && value.length <= 63;
    }

    _validateStaticWep(secret) {
        let value = secret.value;
        if (secret.wep_key_type === NM.WepKeyType.KEY) {
            if (value.length === 10 || value.length === 26) {
                for (let i = 0; i < value.length; i++) {
                    if (!((value[i] >= 'a' && value[i] <= 'f') ||
                          (value[i] >= 'A' && value[i] <= 'F') ||
                          (value[i] >= '0' && value[i] <= '9')))
                        return false;
                }
            } else if (value.length === 5 || value.length === 13) {
                for (let i = 0; i < value.length; i++) {
                    if (!((value[i] >= 'a' && value[i] <= 'z') ||
                          (value[i] >= 'A' && value[i] <= 'Z')))
                        return false;
                }
            } else {
                return false;
            }
        } else if (secret.wep_key_type === NM.WepKeyType.PASSPHRASE) {
            if (value.length < 0 || value.length > 64)
                return false;
        }
        return true;
    }

    _getWirelessSecrets(secrets, _wirelessSetting) {
        let wirelessSecuritySetting = this._connection.get_setting_wireless_security();

        if (this._settingName === '802-1x') {
            this._get8021xSecrets(secrets);
            return;
        }

        switch (wirelessSecuritySetting.key_mgmt) {
        // First the easy ones
        case 'wpa-none':
        case 'wpa-psk':
        case 'sae':
            secrets.push({
                label: _('Password'),
                key: 'psk',
                value: wirelessSecuritySetting.psk || '',
                validate: this._validateWpaPsk,
                password: true,
            });
            break;
        case 'none': // static WEP
            secrets.push({
                label: _('Key'),
                key: `wep-key${wirelessSecuritySetting.wep_tx_keyidx}`,
                value: wirelessSecuritySetting.get_wep_key(wirelessSecuritySetting.wep_tx_keyidx) || '',
                wep_key_type: wirelessSecuritySetting.wep_key_type,
                validate: this._validateStaticWep,
                password: true,
            });
            break;
        case 'ieee8021x':
            if (wirelessSecuritySetting.auth_alg === 'leap') { // Cisco LEAP
                secrets.push({
                    label: _('Password'),
                    key: 'leap-password',
                    value: wirelessSecuritySetting.leap_password || '',
                    password: true,
                });
            } else { // Dynamic (IEEE 802.1x) WEP
                this._get8021xSecrets(secrets);
            }
            break;
        case 'wpa-eap':
            this._get8021xSecrets(secrets);
            break;
        default:
            log(`Invalid wireless key management: ${wirelessSecuritySetting.key_mgmt}`);
        }
    }

    _get8021xSecrets(secrets) {
        let ieee8021xSetting = this._connection.get_setting_802_1x();

        /* If hints were given we know exactly what we need to ask */
        if (this._settingName === '802-1x' && this._hints.length) {
            if (this._hints.includes('identity')) {
                secrets.push({
                    label: _('Username'),
                    key: 'identity',
                    value: ieee8021xSetting.identity || '',
                    password: false,
                });
            }
            if (this._hints.includes('password')) {
                secrets.push({
                    label: _('Password'),
                    key: 'password',
                    value: ieee8021xSetting.password || '',
                    password: true,
                });
            }
            if (this._hints.includes('private-key-password')) {
                secrets.push({
                    label: _('Private key password'),
                    key: 'private-key-password',
                    value: ieee8021xSetting.private_key_password || '',
                    password: true,
                });
            }
            return;
        }

        switch (ieee8021xSetting.get_eap_method(0)) {
        case 'md5':
        case 'leap':
        case 'ttls':
        case 'peap':
        case 'fast':
            // TTLS and PEAP are actually much more complicated, but this complication
            // is not visible here since we only care about phase2 authentication
            // (and don't even care of which one)
            secrets.push({
                label: _('Username'),
                key: null,
                value: ieee8021xSetting.identity || '',
                password: false,
            });
            secrets.push({
                label: _('Password'),
                key: 'password',
                value: ieee8021xSetting.password || '',
                password: true,
            });
            break;
        case 'tls':
            secrets.push({
                label: _('Identity'),
                key: null,
                value: ieee8021xSetting.identity || '',
                password: false,
            });
            secrets.push({
                label: _('Private key password'),
                key: 'private-key-password',
                value: ieee8021xSetting.private_key_password || '',
                password: true,
            });
            break;
        default:
            log(`Invalid EAP/IEEE802.1x method: ${ieee8021xSetting.get_eap_method(0)}`);
        }
    }

    _getPPPoESecrets(secrets) {
        let pppoeSetting = this._connection.get_setting_pppoe();
        secrets.push({
            label: _('Username'),
            key: 'username',
            value: pppoeSetting.username || '',
            password: false,
        });
        secrets.push({
            label: _('Service'), key: 'service',
            value: pppoeSetting.service || '',
            password: false,
        });
        secrets.push({
            label: _('Password'), key: 'password',
            value: pppoeSetting.password || '',
            password: true,
        });
    }

    _getMobileSecrets(secrets, connectionType) {
        let setting;
        if (connectionType === 'bluetooth')
            setting = this._connection.get_setting_cdma() || this._connection.get_setting_gsm();
        else
            setting = this._connection.get_setting_by_name(connectionType);
        secrets.push({
            label: _('Password'),
            key: 'password',
            value: setting.value || '',
            password: true,
        });
    }

    _getContent() {
        let connectionSetting = this._connection.get_setting_connection();
        let connectionType = connectionSetting.get_connection_type();
        let wirelessSetting;
        let ssid;

        let content = { };
        content.secrets = [];

        switch (connectionType) {
        case '802-11-wireless':
            wirelessSetting = this._connection.get_setting_wireless();
            ssid = NM.utils_ssid_to_utf8(wirelessSetting.get_ssid().get_data());
            content.title = _('Authentication required');
            content.message = _('Passwords or encryption keys are required to access the wireless network “%s”.').format(ssid);
            this._getWirelessSecrets(content.secrets, wirelessSetting);
            break;
        case '802-3-ethernet':
            content.title = _('Wired 802.1X authentication');
            content.message = null;
            content.secrets.push({
                label: _('Network name'),
                key: null,
                value: connectionSetting.get_id(),
                password: false,
            });
            this._get8021xSecrets(content.secrets);
            break;
        case 'pppoe':
            content.title = _('DSL authentication');
            content.message = null;
            this._getPPPoESecrets(content.secrets);
            break;
        case 'gsm':
            if (this._hints.includes('pin')) {
                let gsmSetting = this._connection.get_setting_gsm();
                content.title = _('PIN code required');
                content.message = _('PIN code is needed for the mobile broadband device');
                content.secrets.push({
                    label: _('PIN'),
                    key: 'pin',
                    value: gsmSetting.pin || '',
                    password: true,
                });
                break;
            }
            // fall through
        case 'cdma':
        case 'bluetooth':
            content.title = _('Authentication required');
            content.message = _('A password is required to connect to “%s”.').format(connectionSetting.get_id());
            this._getMobileSecrets(content.secrets, connectionType);
            break;
        default:
            log(`Invalid connection type: ${connectionType}`);
        }

        return content;
    }
});

class VPNRequestHandler extends Signals.EventEmitter {
    constructor(agent, requestId, authHelper, serviceType, connection, hints, flags) {
        super();

        this._agent = agent;
        this._requestId = requestId;
        this._connection = connection;
        this._flags = flags;
        this._pluginOutBuffer = [];
        this._title = null;
        this._description = null;
        this._content = [];
        this._shellDialog = null;

        let connectionSetting = connection.get_setting_connection();

        const argv = [
            authHelper.fileName,
            '-u', connectionSetting.uuid,
            '-n', connectionSetting.id,
            '-s', serviceType,
        ];
        if (authHelper.externalUIMode)
            argv.push('--external-ui-mode');
        if (flags & NM.SecretAgentGetSecretsFlags.ALLOW_INTERACTION)
            argv.push('-i');
        if (flags & NM.SecretAgentGetSecretsFlags.REQUEST_NEW)
            argv.push('-r');
        if (authHelper.supportsHints) {
            for (let i = 0; i < hints.length; i++) {
                argv.push('-t');
                argv.push(hints[i]);
            }
        }

        this._newStylePlugin = authHelper.externalUIMode;

        try {
            const launchContext = global.create_app_launch_context(0, -1);
            let [pid, stdin, stdout, stderr] =
                Shell.util_spawn_async_with_pipes(
                    null, /* pwd */
                    argv,
                    launchContext.get_environment(),
                    GLib.SpawnFlags.DO_NOT_REAP_CHILD);

            this._childPid = pid;
            this._stdin = new Gio.UnixOutputStream({fd: stdin, close_fd: true});
            this._stdout = new Gio.UnixInputStream({fd: stdout, close_fd: true});
            GLib.close(stderr);
            this._dataStdout = new Gio.DataInputStream({base_stream: this._stdout});

            if (this._newStylePlugin)
                this._readStdoutNewStyle();
            else
                this._readStdoutOldStyle();

            this._childWatch = GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid,
                this._vpnChildFinished.bind(this));

            this._writeConnection();
        } catch (e) {
            logError(e, 'error while spawning VPN auth helper');

            this._agent.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
        }
    }

    cancel(respond) {
        if (respond)
            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);

        if (this._newStylePlugin && this._shellDialog) {
            this._shellDialog.close(global.get_current_time());
            this._shellDialog.destroy();
        } else {
            try {
                this._stdin.write('QUIT\n\n', null);
            } catch (e) { /* ignore broken pipe errors */ }
        }

        this.destroy();
    }

    destroy() {
        if (this._destroyed)
            return;

        this.emit('destroy');
        if (this._childWatch)
            GLib.source_remove(this._childWatch);

        this._stdin.close(null);
        // Stdout is closed when we finish reading from it

        this._destroyed = true;
    }

    _vpnChildFinished(pid, status, _requestObj) {
        this._childWatch = 0;
        if (this._newStylePlugin) {
            // For new style plugin, all work is done in the async reading functions
            // Just reap the process here
            return;
        }

        let [exited, exitStatus] = Shell.util_wifexited(status);

        if (exited) {
            if (exitStatus !== 0)
                this._agent.respond(this._requestId, Shell.NetworkAgentResponse.USER_CANCELED);
            else
                this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
        } else {
            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
        }

        this.destroy();
    }

    _vpnChildProcessLineOldStyle(line) {
        if (this._previousLine !== undefined) {
            // Two consecutive newlines mean that the child should be closed
            // (the actual newlines are eaten by Gio.DataInputStream)
            // Send a termination message
            if (line === '' && this._previousLine === '') {
                try {
                    this._stdin.write('QUIT\n\n', null);
                } catch (e) { /* ignore broken pipe errors */ }
            } else {
                this._agent.add_vpn_secret(this._requestId, this._previousLine, line);
                this._previousLine = undefined;
            }
        } else {
            this._previousLine = line;
        }
    }

    async _readStdoutOldStyle() {
        const [line, len_] =
            await this._dataStdout.read_line_async(GLib.PRIORITY_DEFAULT, null);

        if (line === null) {
            // end of file
            this._stdout.close(null);
            return;
        }

        const decoder = new TextDecoder();
        this._vpnChildProcessLineOldStyle(decoder.decode(line));

        // try to read more!
        this._readStdoutOldStyle();
    }

    async _readStdoutNewStyle() {
        const cnt =
            await this._dataStdout.fill_async(-1, GLib.PRIORITY_DEFAULT, null);

        if (cnt === 0) {
            // end of file
            this._showNewStyleDialog();

            this._stdout.close(null);
            return;
        }

        // Try to read more
        this._dataStdout.set_buffer_size(2 * this._dataStdout.get_buffer_size());
        this._readStdoutNewStyle();
    }

    _showNewStyleDialog() {
        let keyfile = new GLib.KeyFile();
        let data;
        let contentOverride;

        try {
            data = new GLib.Bytes(this._dataStdout.peek_buffer());
            keyfile.load_from_bytes(data, GLib.KeyFileFlags.NONE);

            if (keyfile.get_integer(VPN_UI_GROUP, 'Version') !== 2)
                throw new Error('Invalid plugin keyfile version, is %d');

            contentOverride = {
                title: keyfile.get_string(VPN_UI_GROUP, 'Title'),
                message: keyfile.get_string(VPN_UI_GROUP, 'Description'),
                secrets: [],
            };

            let [groups, len_] = keyfile.get_groups();
            for (let i = 0; i < groups.length; i++) {
                if (groups[i] === VPN_UI_GROUP)
                    continue;

                let value = keyfile.get_string(groups[i], 'Value');
                let shouldAsk = keyfile.get_boolean(groups[i], 'ShouldAsk');

                if (shouldAsk) {
                    contentOverride.secrets.push({
                        label: keyfile.get_string(groups[i], 'Label'),
                        key: groups[i],
                        value,
                        password: keyfile.get_boolean(groups[i], 'IsSecret'),
                    });
                } else {
                    if (!value.length) // Ignore empty secrets
                        continue;

                    this._agent.add_vpn_secret(this._requestId, groups[i], value);
                }
            }
        } catch (e) {
            // No output is a valid case it means "both secrets are stored"
            if (data.length > 0) {
                logError(e, 'error while reading VPN plugin output keyfile');

                this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
                this.destroy();
                return;
            }
        }

        if (contentOverride && contentOverride.secrets.length) {
            // Only show the dialog if we actually have something to ask
            this._shellDialog = new NetworkSecretDialog(this._agent, this._requestId, this._connection, 'vpn', [], this._flags, contentOverride);
            this._shellDialog.open();
        } else {
            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.CONFIRMED);
            this.destroy();
        }
    }

    _writeConnection() {
        let vpnSetting = this._connection.get_setting_vpn();

        try {
            vpnSetting.foreach_data_item((key, value) => {
                this._stdin.write(`DATA_KEY=${key}\n`, null);
                this._stdin.write(`DATA_VAL=${value || ''}\n\n`, null);
            });
            vpnSetting.foreach_secret((key, value) => {
                this._stdin.write(`SECRET_KEY=${key}\n`, null);
                this._stdin.write(`SECRET_VAL=${value || ''}\n\n`, null);
            });
            this._stdin.write('DONE\n\n', null);
        } catch (e) {
            logError(e, 'internal error while writing connection to helper');

            this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
            this.destroy();
        }
    }
}

class NetworkAgent {
    constructor() {
        this._native = new Shell.NetworkAgent({
            identifier: 'org.gnome.Shell.NetworkAgent',
            capabilities: NM.SecretAgentCapabilities.VPN_HINTS,
            auto_register: false,
        });

        this._dialogs = { };
        this._vpnRequests = { };
        this._notifications = { };

        this._native.connect('new-request', this._newRequest.bind(this));
        this._native.connect('cancel-request', this._cancelRequest.bind(this));

        this._initialized = false;
        this._initNative();
    }

    async _initNative() {
        try {
            await this._native.init_async(GLib.PRIORITY_DEFAULT, null);
            this._initialized = true;
        } catch (e) {
            this._native = null;
            logError(e, 'error initializing the NetworkManager Agent');
        }
    }

    enable() {
        if (!this._native)
            return;

        this._native.auto_register = true;
        if (this._initialized && !this._native.registered)
            this._native.register_async(null, null);
    }

    disable() {
        let requestId;

        for (requestId in this._dialogs)
            this._dialogs[requestId].cancel();
        this._dialogs = { };

        for (requestId in this._vpnRequests)
            this._vpnRequests[requestId].cancel(true);
        this._vpnRequests = { };

        for (requestId in this._notifications)
            this._notifications[requestId].destroy();
        this._notifications = { };

        if (!this._native)
            return;

        this._native.auto_register = false;
        if (this._initialized && this._native.registered)
            this._native.unregister_async(null, null);
    }

    _showNotification(requestId, connection, settingName, hints, flags) {
        let title, body;

        let connectionSetting = connection.get_setting_connection();
        let connectionType = connectionSetting.get_connection_type();
        switch (connectionType) {
        case '802-11-wireless': {
            let wirelessSetting = connection.get_setting_wireless();
            let ssid = NM.utils_ssid_to_utf8(wirelessSetting.get_ssid().get_data());
            title = _('Authentication required');
            body = _('Passwords or encryption keys are required to access the wireless network “%s”.').format(ssid);
            break;
        }
        case '802-3-ethernet':
            title = _('Wired 802.1X authentication');
            body = _('A password is required to connect to “%s”.').format(connection.get_id());
            break;
        case 'pppoe':
            title = _('DSL authentication');
            body = _('A password is required to connect to “%s”.').format(connection.get_id());
            break;
        case 'gsm':
            if (hints.includes('pin')) {
                title = _('PIN code required');
                body = _('PIN code is needed for the mobile broadband device');
                break;
            }
            // fall through
        case 'cdma':
        case 'bluetooth':
            title = _('Authentication required');
            body = _('A password is required to connect to “%s”.').format(connectionSetting.get_id());
            break;
        case 'vpn':
            title = _('VPN password');
            body = _('A password is required to connect to “%s”.').format(connectionSetting.get_id());
            break;
        default:
            log(`Invalid connection type: ${connectionType}`);
            this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
            return;
        }

        const source = MessageTray.getSystemSource();
        const notification = new MessageTray.Notification({source, title, body});
        notification.iconName = 'dialog-password-symbolic';

        notification.connect('activated', () => {
            notification.answered = true;
            this._handleRequest(requestId, connection, settingName, hints, flags);
        });

        this._notifications[requestId] = notification;
        notification.connect('destroy', () => {
            if (!notification.answered)
                this._native.respond(requestId, Shell.NetworkAgentResponse.USER_CANCELED);
            delete this._notifications[requestId];
        });

        source.addNotification(notification);
    }

    _newRequest(agent, requestId, connection, settingName, hints, flags) {
        if (!(flags & NM.SecretAgentGetSecretsFlags.USER_REQUESTED))
            this._showNotification(requestId, connection, settingName, hints, flags);
        else
            this._handleRequest(requestId, connection, settingName, hints, flags);
    }

    _handleRequest(requestId, connection, settingName, hints, flags) {
        if (settingName === 'vpn') {
            this._vpnRequest(requestId, connection, hints, flags);
            return;
        }

        let dialog = new NetworkSecretDialog(this._native, requestId, connection, settingName, hints, flags);
        dialog.connect('destroy', () => {
            delete this._dialogs[requestId];
        });
        this._dialogs[requestId] = dialog;
        dialog.open();
    }

    _cancelRequest(agent, requestId) {
        if (this._dialogs[requestId]) {
            this._dialogs[requestId].close(global.get_current_time());
            this._dialogs[requestId].destroy();
            delete this._dialogs[requestId];
        } else if (this._vpnRequests[requestId]) {
            this._vpnRequests[requestId].cancel(false);
            delete this._vpnRequests[requestId];
        }
    }

    async _vpnRequest(requestId, connection, hints, flags) {
        let vpnSetting = connection.get_setting_vpn();
        let serviceType = vpnSetting.service_type;

        let binary = await this._findAuthBinary(serviceType);
        if (!binary) {
            log('Invalid VPN service type (cannot find authentication binary)');

            /* cancel the auth process */
            this._native.respond(requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
            return;
        }

        let vpnRequest = new VPNRequestHandler(this._native, requestId, binary, serviceType, connection, hints, flags);
        vpnRequest.connect('destroy', () => {
            delete this._vpnRequests[requestId];
        });
        this._vpnRequests[requestId] = vpnRequest;
    }

    async _findAuthBinary(serviceType) {
        let plugin;

        try {
            plugin = await this._native.search_vpn_plugin(serviceType);
        } catch (e) {
            logError(e);
            return null;
        }

        const fileName = plugin.get_auth_dialog();
        if (!GLib.file_test(fileName, GLib.FileTest.IS_EXECUTABLE)) {
            log(`VPN plugin at ${fileName} is not executable`);
            return null;
        }

        const prop = plugin.lookup_property('GNOME', 'supports-external-ui-mode');
        const trimmedProp = prop?.trim().toLowerCase() ?? '';

        return {
            fileName,
            supportsHints: plugin.supports_hints(),
            externalUIMode: ['true', 'yes', 'on', '1'].includes(trimmedProp),
        };
    }
}

export {NetworkAgent as Component};
(uuay)signals.jsimport * as SignalTracker from './signalTracker.js';

const Signals = imports.signals;

export class EventEmitter {
    connectObject(...args) {
        return SignalTracker.connectObject(this, ...args);
    }

    disconnectObject(...args) {
        return SignalTracker.disconnectObject(this, ...args);
    }

    connect_object(...args) {
        return this.connectObject(...args);
    }

    disconnect_object(...args) {
        return this.disconnectObject(...args);
    }
}

Signals.addSignalMethods(EventEmitter.prototype);
(uuay)util.js��// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gdm from 'gi://Gdm';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import * as Signals from '../misc/signals.js';

import * as Authd from './authd.js';
import * as Batch from './batch.js';
import * as OVirt from './oVirt.js';
import * as Vmware from './vmware.js';
import * as Main from '../ui/main.js';
import {loadInterfaceXML} from '../misc/fileUtils.js';
import * as Params from '../misc/params.js';
import * as SmartcardManager from '../misc/smartcardManager.js';
import * as UnifiedMechanism from './unifiedMechanism.js';
import * as Const from './const.js';

const FprintManagerInfo = Gio.DBusInterfaceInfo.new_for_xml(
    loadInterfaceXML('net.reactivated.Fprint.Manager'));
const FprintDeviceInfo = Gio.DBusInterfaceInfo.new_for_xml(
    loadInterfaceXML('net.reactivated.Fprint.Device'));

Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel');
Gio._promisify(Gdm.Client.prototype, 'get_user_verifier');
Gio._promisify(Gdm.UserVerifierProxy.prototype,
    'call_begin_verification_for_user');
Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification');

export const UNIFIED_AUTH_SERVICE_NAME = 'gdm-authd';
export const PASSWORD_SERVICE_NAME = 'gdm-password';
export const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint';
export const SMARTCARD_SERVICE_NAME = 'gdm-smartcard';
const CLONE_FADE_ANIMATION_TIME = 250;

export const PASSWORD_ROLE_NAME = Const.PASSWORD_ROLE_NAME;
export const SMARTCARD_ROLE_NAME = Const.SMARTCARD_ROLE_NAME;
export const FINGERPRINT_ROLE_NAME = Const.FINGERPRINT_ROLE_NAME;
export const WEB_LOGIN_ROLE_NAME = Const.WEB_LOGIN_ROLE_NAME;

export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';
export const UNIFIED_AUTHENTICATION_KEY = 'enable-unified-authentication';
export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication';
export const WEB_LOGIN_AUTHENTICATION_KEY = 'enable-web-authentication';
export const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication';
export const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication';
export const BANNER_MESSAGE_KEY = 'banner-message-enable';
export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text';
export const ALLOWED_FAILURES_KEY = 'allowed-failures';

export const UBUNTU_LOGIN_SCREEN_SCHEMA = 'com.ubuntu.login-screen';
export const UBUNTU_AUTHD_AUTHENTICATION_KEY = 'enable-authd-authentication';

export const LOGO_KEY = 'logo';
export const DISABLE_USER_LIST_KEY = 'disable-user-list';

// Give user 48ms to read each character of a PAM message
const USER_READ_TIME = 48;
const FINGERPRINT_SERVICE_PROXY_TIMEOUT = 5000;
const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15;

// Note these are specified in order of preference
const DiscreteServiceMechanismDefinitions = [
    {
        serviceName: PASSWORD_SERVICE_NAME,
        protocol: 'discrete',
        mechanismId: 'password',
        mechanismName: _('Password'),
        setting: PASSWORD_AUTHENTICATION_KEY,
        role: PASSWORD_ROLE_NAME,
        selectable: true,
    },

    {
        serviceName: SMARTCARD_SERVICE_NAME,
        protocol: 'discrete',
        mechanismId: 'smartcard',
        mechanismName: _('Smartcard'),
        setting: SMARTCARD_AUTHENTICATION_KEY,
        role: SMARTCARD_ROLE_NAME,
        selectable: true,
    },

    {
        serviceName: FINGERPRINT_SERVICE_NAME,
        protocol: 'discrete',
        mechanismId: 'fingerprint',
        mechanismName: _('Fingerprint'),
        setting: FINGERPRINT_AUTHENTICATION_KEY,
        role: FINGERPRINT_ROLE_NAME,
        selectable: false,
    },
];

/**
 * Keep messages in order by priority
 *
 * @enum {number}
 */
export const MessageType = {
    NONE: 0,
    HINT: 1,
    INFO: 2,
    ERROR: 3,
    BACKGROUND: 4,
};

const FingerprintReaderType = {
    NONE: 0,
    PRESS: 1,
    SWIPE: 2,
};

/**
 * @param {Clutter.Actor} actor
 */
export function cloneAndFadeOutActor(actor) {
    // Immediately hide actor so its sibling can have its space
    // and position, but leave a non-reactive clone on-screen,
    // so from the user's point of view it smoothly fades away
    // and reveals its sibling.
    actor.hide();

    const clone = new Clutter.Clone({
        source: actor,
        reactive: false,
    });

    Main.uiGroup.add_child(clone);

    let [x, y] = actor.get_transformed_position();
    clone.set_position(x, y);

    let hold = new Batch.Hold();
    clone.ease({
        opacity: 0,
        duration: CLONE_FADE_ANIMATION_TIME,
        mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        onComplete: () => {
            clone.destroy();
            hold.release();
        },
    });
    return hold;
}

export class ShellUserVerifier extends Signals.EventEmitter {
    constructor(client, params) {
        super();
        params = Params.parse(params, {reauthenticationOnly: false});
        this._reauthOnly = params.reauthenticationOnly;

        this._client = client;
        this._cancellable = null;

        this._defaultService = null;
        this._preemptingService = null;
        this._foregroundMechanism = null;
        this._fingerprintReaderType = FingerprintReaderType.NONE;

        this._messageQueue = [];
        this._messageQueueTimeoutId = 0;

        this._failCounter = 0;
        this._activeServices = new Set();
        this._unavailableServices = new Set();
        this._activeServices = new Set();
        this._publishedMechanisms = new Map();
        this._pendingMechanisms = new Map();

        this._unifiedAuthServices = new Map();
        this.addUnifiedAuthService(new UnifiedMechanism.UnifiedMechanismProtocolHandler());
        this.addUnifiedAuthService(new Authd.AuthdSwitchableHandler());

        this._credentialManagers = {};

        this.reauthenticating = false;
        this.smartcardDetected = false;

        this._settings = new Gio.Settings({schema_id: LOGIN_SCREEN_SCHEMA});
        this._settings.connect('changed', () => this._onSettingsChanged());
        this._ubuntuSettings = new Gio.Settings({schema_id: UBUNTU_LOGIN_SCREEN_SCHEMA});
        this._ubuntuSettings.connectObject('changed', () => this._onSettingsChanged(), this);
        this._updateEnabledServices();
        this._updateDefaultService();

        this.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager());
        this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager());
    }

    addUnifiedAuthService(unifiedService) {
        /* TODO: check if not implements too */
        if (!unifiedService || !unifiedService.protocolName)
            throw new Error('Invalid unified AuthService');

        const handler = this._unifiedAuthServices.get(unifiedService.protocolName);
        if (handler && handler !== unifiedService)
            throw new Error(`Protocol ${unifiedService.protocolName} is already handled`);

        this._unifiedAuthServices.set(unifiedService.protocolName, unifiedService);
        this._monitorUnifiedServiceMechanisms(unifiedService);
    }

    _monitorUnifiedServiceMechanisms(unifiedService) {
        const serviceName = UNIFIED_AUTH_SERVICE_NAME;
        unifiedService.connectObject('mechanisms-changed', (_, mechanisms) => {
            let mechanismsList = [];
            for (const id of Object.keys(mechanisms)) {
                const name = mechanisms[id].name;
                const role = mechanisms[id].role;

                if (!name || !role)
                    continue;

                const protocol = unifiedService.protocolName;
                const selectable = mechanisms[id].selectable ?? true;
                mechanisms[id].protocol = protocol;
                mechanismsList.push({id, name, role, serviceName, protocol, selectable});
            }

            // FIXME: invert the logic, using mechanismsList.filter() instead.
            mechanismsList = this._filterAuthMechanisms(mechanismsList, m => {
                delete mechanisms[m.id];
            });

            if (unifiedService.sortMechanisms)
                mechanismsList = mechanismsList.sort((a, b) =>
                    unifiedService.sortMechanisms(a, b));

            this._publishedMechanisms.set(serviceName, mechanisms);
            this.emit('mechanisms-list-changed', serviceName, mechanismsList);
        }, this);

        unifiedService.connectObject('start-mechanism',
            (_, ...args) => this._startMechanismFromUnifiedService(...args), this);
        unifiedService.connectObject('choice-list-selected',
            (_, ...args) => this.emit('choice-list-selected', serviceName, ...args), this);
        unifiedService.connectObject('queue-message',
            (_, ...args) => this._queueMessage(serviceName, ...args), this);
        unifiedService.connectObject('queue-priority-message',
            (_, ...args) => this._queuePriorityMessage(serviceName, ...args), this);
        unifiedService.connectObject('clear-message-queue',
            (_, ...args) => this._clearMessageQueue(...args), this);
        unifiedService.connectObject('verification-complete',
            () => this.emit('verification-complete'), this);
        unifiedService.connectObject('service-request',
            (_, ...args) => this.emit(`service-request::${serviceName}`, ...args), this);
        unifiedService.connectObject('reset', () => this.emit('reset'), this);
        unifiedService.connectObject('cancel', () => this.cancel(), this);
    }

    addCredentialManager(serviceName, credentialManager) {
        if (this._credentialManagers[serviceName])
            return;

        this._credentialManagers[serviceName] = credentialManager;
        if (credentialManager.token) {
            this._onCredentialManagerAuthenticated(credentialManager,
                credentialManager.token);
        }

        credentialManager.connectObject('user-authenticated',
            this._onCredentialManagerAuthenticated.bind(this), this);
    }

    removeCredentialManager(serviceName) {
        let credentialManager = this._credentialManagers[serviceName];
        if (!credentialManager)
            return;

        credentialManager.disconnectObject(this);
        delete this._credentialManagers[serviceName];
    }

    get hasPendingMessages() {
        return !!this._messageQueue.length;
    }

    get allowedFailures() {
        return this._settings.get_int(ALLOWED_FAILURES_KEY);
    }

    get currentMessage() {
        return this._messageQueue ? this._messageQueue[0] : null;
    }

    begin(userName, hold) {
        this._cancellable = new Gio.Cancellable();
        this._hold = hold;
        this._userName = userName;
        this.reauthenticating = false;

        this._checkForFingerprintReader().catch(e =>
            this._handleFingerprintError(e));

        // If possible, reauthenticate an already running session,
        // so any session specific credentials get updated appropriately
        if (userName)
            this._openReauthenticationChannel(userName).catch(logError);
        else
            this._getUserVerifier();
    }

    cancel() {
        if (this._cancellable)
            this._cancellable.cancel();

        [...this._unifiedAuthServices.values()].forEach(s => s.cancel());
        if (this._mechanismsListChangedSignalId) {
            this.disconnect(this._mechanismsListChangedSignalId);
            this._mechanismsListChangedSignalId = 0;
        }

        if (this._userVerifier) {
            this._userVerifier.call_cancel_sync(null);
            this.clear();
        }
    }

    _clearUserVerifier() {
        this._pendingMechanisms.clear();
        if (this._userVerifier) {
            this._disconnectSignals();
            this._userVerifier.run_dispose();
            this._userVerifier = null;
            if (this._userVerifierChoiceList) {
                this._userVerifierChoiceList.run_dispose();
                this._userVerifierChoiceList = null;
            }
            if (this._userVerifierCustomJSON) {
                this._userVerifierCustomJSON.run_dispose();
                this._userVerifierCustomJSON = null;
            }
        }
    }

    clear() {
        if (this._cancellable) {
            this._cancellable.cancel();
            this._cancellable = null;
        }

        this._clearWebLoginTimeout();
        this._clearUserVerifier();
        this._clearMessageQueue();
        this._activeServices.clear();
    }

    destroy() {
        this.cancel();

        this._settings.run_dispose();
        this._settings = null;

        this._smartcardManager?.disconnectObject(this);
        this._smartcardManager = null;

        this._fingerprintManager = null;
        this._unifiedAuthServices.forEach(s => s.disconnectObject(this));
        this._unifiedAuthServices.clear();

        for (let service in this._credentialManagers)
            this.removeCredentialManager(service);
    }

    cancelRequested() {
        return [...this._unifiedAuthServices.values()].some(service =>
            service.cancelRequested());
    }

    selectChoice(serviceName, key) {
        if (serviceName === UNIFIED_AUTH_SERVICE_NAME &&
            this._pendingMechanisms.has(Const.CHOICE_LIST_ROLE_NAME)) {
            this._replyWithAuthSelectionResponse(serviceName, Const.CHOICE_LIST_ROLE_NAME, key);
            return;
        }

        this._userVerifierChoiceList.call_select_choice(serviceName, key, this._cancellable, null);
    }

    async answerQuery(serviceName, answer) {
        try {
            await this._handlePendingMessages();
            if (this._pendingMechanisms.has(PASSWORD_ROLE_NAME))
                this._replyWithAuthSelectionResponse(serviceName, PASSWORD_ROLE_NAME, {password: answer});
            else if (this._pendingMechanisms.has(Const.PLAIN_TEXT_ROLE_NAME))
                this._replyWithAuthSelectionResponse(serviceName, Const.PLAIN_TEXT_ROLE_NAME, {text: answer});
            else
                this._userVerifier.call_answer_query(serviceName, answer, this._cancellable, null);
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                logError(e);
        }
    }

    async _replyWithJSON(serviceName, json) {
        try {
            await this._handlePendingMessages();
        } catch (e) {
            logError(e);
            this._userVerifierCustomJSON?.call_report_error(serviceName,
                `Unexpected error: ${e}`, this._cancellable, null);
            return;
        }

        try {
            this._userVerifierCustomJSON.call_reply(serviceName, json, this._cancellable, null);
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                logError(e);
        }
    }

    _getIntervalForMessage(message) {
        if (!message)
            return 0;

        // We probably could be smarter here
        return message.length * USER_READ_TIME;
    }

    finishMessageQueue() {
        if (!this.hasPendingMessages)
            return;

        this._messageQueue = [];

        this.emit('no-more-messages');
    }

    increaseCurrentMessageTimeout(interval) {
        if (!this._messageQueueTimeoutId && interval > 0)
            this._currentMessageExtraInterval = interval;
    }

    _serviceHasPendingMessages(serviceName) {
        return this._messageQueue.some(m => m.serviceName === serviceName);
    }

    _filterServiceMessages(serviceName, messageType) {
        // This function allows to remove queued messages for the @serviceName
        // whose type has lower priority than @messageType, replacing them
        // with a null message that will lead to clearing the prompt once done.
        if (this._serviceHasPendingMessages(serviceName))
            this._queuePriorityMessage(serviceName, null, messageType);
    }

    _queueMessageTimeout() {
        if (this._messageQueueTimeoutId !== 0)
            return;

        const message = this.currentMessage;

        delete this._currentMessageExtraInterval;
        this.emit('show-message', message.serviceName, message.text, message.type);

        this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
            message.interval + (this._currentMessageExtraInterval | 0), () => {
                this._messageQueueTimeoutId = 0;

                if (this._messageQueue.length > 1) {
                    this._messageQueue.shift();
                    this._queueMessageTimeout();
                } else {
                    this.finishMessageQueue();
                }

                return GLib.SOURCE_REMOVE;
            });
        GLib.Source.set_name_by_id(this._messageQueueTimeoutId, '[gnome-shell] this._queueMessageTimeout');
    }

    _queueMessage(serviceName, message, messageType) {
        let interval = this._getIntervalForMessage(message);

        this._messageQueue.push({serviceName, text: message, type: messageType, interval});
        this._queueMessageTimeout();
    }

    _queuePriorityMessage(serviceName, message, messageType) {
        const newQueue = this._messageQueue.filter(m => {
            if (m.serviceName !== serviceName || m.type >= messageType)
                return m.text !== message;
            return false;
        });

        if (!newQueue.includes(this.currentMessage))
            this._clearMessageQueue();

        this._messageQueue = newQueue;
        this._queueMessage(serviceName, message, messageType);
    }

    _clearMessageQueue() {
        this.finishMessageQueue();

        if (this._messageQueueTimeoutId !== 0) {
            GLib.source_remove(this._messageQueueTimeoutId);
            this._messageQueueTimeoutId = 0;
        }
        this.emit('show-message', null, null, MessageType.NONE);
    }

    async _initFingerprintManager() {
        if (this._fprintManager)
            return;

        const fprintManager = new Gio.DBusProxy({
            g_connection: Gio.DBus.system,
            g_name: 'net.reactivated.Fprint',
            g_object_path: '/net/reactivated/Fprint/Manager',
            g_interface_name: FprintManagerInfo.name,
            g_interface_info: FprintManagerInfo,
            g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES |
                Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION |
                Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS,
        });

        try {
            if (!this._getDetectedDefaultService()) {
                // Other authentication methods would have already been detected by
                // now as possibilities if they were available.
                // If we're here it means that FINGERPRINT_AUTHENTICATION_KEY is
                // true and so fingerprint authentication is our last potential
                // option, so go ahead a synchronously look for a fingerprint device
                // during startup or default service update.
                fprintManager.init(null);
                // Do not wait too much for fprintd to reply, as in case it hangs
                // we should fail early without having the shell to misbehave
                fprintManager.set_default_timeout(FINGERPRINT_SERVICE_PROXY_TIMEOUT);

                const [devicePath] = fprintManager.GetDefaultDeviceSync();
                this._fprintManager = fprintManager;

                const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath);
                fprintDeviceProxy.init(null);
                this._setFingerprintReaderType(fprintDeviceProxy['scan-type']);
            } else {
                // Ensure fingerprint service starts, but do not wait for it
                const cancellable = this._cancellable;
                await fprintManager.init_async(GLib.PRIORITY_DEFAULT, cancellable);
                await this._updateFingerprintReaderType(fprintManager, cancellable);
                this._fprintManager = fprintManager;
            }
        } catch (e) {
            this._handleFingerprintError(e);
        }
    }

    _getFingerprintDeviceProxy(devicePath) {
        return new Gio.DBusProxy({
            g_connection: Gio.DBus.system,
            g_name: 'net.reactivated.Fprint',
            g_object_path: devicePath,
            g_interface_name: FprintDeviceInfo.name,
            g_interface_info: FprintDeviceInfo,
            g_flags: Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS,
        });
    }

    _handleFingerprintError(e) {
        this._fingerprintReaderType = FingerprintReaderType.NONE;

        if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
            return;
        if (e.matches(Gio.DBusError, Gio.DBusError.SERVICE_UNKNOWN))
            return;

        if (Gio.DBusError.is_remote_error(e) &&
            Gio.DBusError.get_remote_error(e) ===
                'net.reactivated.Fprint.Error.NoSuchDevice')
            return;

        logError(e, 'Failed to interact with fprintd service');
    }

    async _checkForFingerprintReader() {
        if (!this._fprintManager) {
            this._updateDefaultService();
            return;
        }

        if (this._fingerprintReaderType !== FingerprintReaderType.NONE)
            return;

        await this._updateFingerprintReaderType(this._fprintManager, this._cancellable);
    }

    async _updateFingerprintReaderType(fprintManager, cancellable) {
        // Wrappers don't support null cancellable, so let's ignore it in case
        const args = cancellable ? [cancellable] : [];
        const [devicePath] = await fprintManager.GetDefaultDeviceAsync(...args);
        const fprintDeviceProxy = this._getFingerprintDeviceProxy(devicePath);
        await fprintDeviceProxy.init_async(GLib.PRIORITY_DEFAULT, cancellable);
        this._setFingerprintReaderType(fprintDeviceProxy['scan-type']);
        this._updateDefaultService();

        if (this._userVerifier &&
            !this._activeServices.has(FINGERPRINT_SERVICE_NAME)) {
            if (!this._hold?.isAcquired())
                this._hold = new Batch.Hold();
            await this._maybeStartFingerprintVerification();
        }
    }

    _setFingerprintReaderType(fprintDeviceType) {
        this._fingerprintReaderType =
            FingerprintReaderType[fprintDeviceType.toUpperCase()];

        if (this._fingerprintReaderType === undefined)
            throw new Error(`Unexpected fingerprint device type '${fprintDeviceType}'`);
    }

    _onCredentialManagerAuthenticated(credentialManager, _token) {
        this.setForegroundService(credentialManager.service);
        this.emit('credential-manager-authenticated');
    }

    _initSmartcardManager() {
        if (this._smartcardManager)
            return;

        this._smartcardManager = SmartcardManager.getSmartcardManager();

        // We check for smartcards right away, since an inserted smartcard
        // at startup should result in immediately initiating authentication.
        // This is different than fingerprint readers, where we only check them
        // after a user has been picked.
        this.smartcardDetected = false;
        this._checkForSmartcard();

        this._smartcardManager.connectObject(
            'smartcard-inserted', () => this._checkForSmartcard(),
            'smartcard-removed', () => this._checkForSmartcard(), this);
    }

    _checkForSmartcard() {
        let smartcardDetected;

        if (!this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY))
            smartcardDetected = false;
        else if (this._reauthOnly)
            smartcardDetected = this._smartcardManager.hasInsertedLoginToken();
        else
            smartcardDetected = this._smartcardManager.hasInsertedTokens();

        if (smartcardDetected !== this.smartcardDetected) {
            this.smartcardDetected = smartcardDetected;

            if (this.smartcardDetected)
                this.setForegroundService(SMARTCARD_SERVICE_NAME);
            else if (this._preemptingService === SMARTCARD_SERVICE_NAME)
                this.setForegroundService(null);

            this.emit('smartcard-status-changed');
        }
    }

    _reportInitError(where, error, serviceName) {
        logError(error, where);
        this._hold.release();

        this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR);
        this._failCounter++;
        this._verificationFailed(serviceName, false);
    }

    _getClientExtensionProxies() {
        if (this._client.get_user_verifier_choice_list)
            this._userVerifierChoiceList = this._client.get_user_verifier_choice_list();
        else
            this._userVerifierChoiceList = null;

        if (this._client.get_user_verifier_custom_json)
            this._userVerifierCustomJSON = this._client.get_user_verifier_custom_json();
        else
            this._userVerifierCustomJSON = null;
    }

    async _openReauthenticationChannel(userName) {
        try {
            this._clearUserVerifier();
            this._userVerifier = await this._client.open_reauthentication_channel(
                userName, this._cancellable);
        } catch (e) {
            if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                return;
            if (e.matches(Gio.DBusError, Gio.DBusError.ACCESS_DENIED) &&
                !this._reauthOnly) {
                // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there
                // is no session to reauthenticate. Fall back to performing
                // verification from this login session
                this._getUserVerifier();
                return;
            }

            this._reportInitError('Failed to open reauthentication channel', e);
            return;
        }

        this._getClientExtensionProxies();

        this.reauthenticating = true;
        this._connectSignals();
        this._beginVerification();
        this._hold.release();
    }

    async _getUserVerifier() {
        try {
            this._clearUserVerifier();
            this._userVerifier =
                await this._client.get_user_verifier(this._cancellable);
        } catch (e) {
            if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                return;
            this._reportInitError('Failed to obtain user verifier', e);
            return;
        }

        this._getClientExtensionProxies();

        this._connectSignals();
        this._beginVerification();
        this._hold.release();
    }

    _connectSignals() {
        this._disconnectSignals();

        this._userVerifier.connectObject(
            'info', this._onInfo.bind(this),
            'problem', this._onProblem.bind(this),
            'info-query', this._onInfoQuery.bind(this),
            'secret-info-query', this._onSecretInfoQuery.bind(this),
            'conversation-started', this._onConversationStarted.bind(this),
            'conversation-stopped', this._onConversationStopped.bind(this),
            'service-unavailable', this._onServiceUnavailable.bind(this),
            'reset', this._onReset.bind(this),
            'verification-complete', this._onVerificationComplete.bind(this),
            this);

        if (this._userVerifierChoiceList) {
            this._userVerifierChoiceList.connectObject('choice-query',
                this._onChoiceListQuery.bind(this), this);
        }

        if (this._userVerifierCustomJSON) {
            this._userVerifierCustomJSON.connectObject('request',
                this._onCustomJSONRequest.bind(this), this);
        }
    }

    _disconnectSignals() {
        this._userVerifier?.disconnectObject(this);
        this._userVerifierChoiceList?.disconnectObject(this);
        this._userVerifierCustomJSON?.disconnectObject(this);
    }

    _getForegroundService() {
        if (this._preemptingService)
            return this._preemptingService;

        return this._defaultService;
    }

    setForegroundService(serviceName) {
        if (this._getForegroundService() === serviceName)
            return;

        this._preemptingService = serviceName;
        this.emit('foreground-service-changed');
    }

    _getForegroundMechanism() {
        if (this._foregroundMechanism)
            return this._foregroundMechanism;

        const foregroundService = this._getForegroundService();
        if (foregroundService === this._defaultService) {
            const definition = DiscreteServiceMechanismDefinitions.find(
                def => def.serviceName === foregroundService);

            if (!definition)
                return null;

            const {mechanismName, mechanismId, role, serviceName, protocol} = definition;

            return {name: mechanismName, id: mechanismId, role, serviceName, protocol};
        }

        return null;
    }

    setForegroundMechanism(mechanism) {
        const foregroundMechanism = this._getForegroundMechanism();

        this._foregroundMechanism = mechanism;

        if (foregroundMechanism === mechanism)
            return;

        if (foregroundMechanism?.role === mechanism?.role &&
            foregroundMechanism?.id === mechanism?.id &&
            foregroundMechanism?.mechanismName === mechanism?.mechanismName &&
            foregroundMechanism?.serviceName === mechanism?.serviceName)
            return;

        if (mechanism.serviceName === UNIFIED_AUTH_SERVICE_NAME) {
            const unifiedService = this._unifiedAuthServices.get(mechanism.protocol);
            if (unifiedService.setForegroundMechanism(mechanism)) {
                this._foregroundMechanism = mechanism;
                return;
            }
        }

        this.emit('foreground-mechanism-changed');
    }

    serviceIsForeground(serviceName) {
        return serviceName === this._getForegroundService();
    }

    foregroundServiceDeterminesUsername() {
        for (let serviceName in this._credentialManagers) {
            if (this.serviceIsForeground(serviceName))
                return true;
        }

        return this.serviceIsForeground(SMARTCARD_SERVICE_NAME);
    }

    serviceIsDefault(serviceName) {
        return serviceName === this._defaultService;
    }

    serviceIsFingerprint(serviceName) {
        return this._fingerprintReaderType !== FingerprintReaderType.NONE &&
            serviceName === FINGERPRINT_SERVICE_NAME;
    }

    _onSettingsChanged() {
        this._updateEnabledServices();
        this._updateDefaultService();
    }

    _updateEnabledServices() {
        let needsReset = false;

        if (this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY)) {
            this._initFingerprintManager().catch(logError);
        } else if (this._fingerprintManager) {
            this._fingerprintManager = null;
            this._fingerprintReaderType = FingerprintReaderType.NONE;

            if (this._activeServices.has(FINGERPRINT_SERVICE_NAME))
                needsReset = true;
        }

        if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY)) {
            this._initSmartcardManager();
        } else if (this._smartcardManager) {
            this._smartcardManager.disconnectObject(this);
            this._smartcardManager = null;

            if (this._activeServices.has(SMARTCARD_SERVICE_NAME))
                needsReset = true;
        }

        if (needsReset)
            this._cancelAndReset();
    }

    _isDiscreteServiceEnabled(definition) {
        switch (definition.serviceName) {
        case PASSWORD_AUTHENTICATION_KEY:
            return this._settings.get_boolean(definition.setting);
        case FINGERPRINT_AUTHENTICATION_KEY:
            return this._fingerprintReaderType !== FingerprintReaderType.NONE;
        case SMARTCARD_SERVICE_NAME:
            return !!this._smartcardManager;
        default:
            return false;
        }
    }

    _serviceIsAvailable(serviceName) {
        return GLib.file_test(`/etc/pam.d/${serviceName}`, GLib.FileTest.EXISTS) ||
            GLib.file_test(`/usr/lib/pam.d/${serviceName}`, GLib.FileTest.EXISTS)
    }

    _getDetectedDefaultService() {
        if ((!this._settings.settingsSchema.has_key(UNIFIED_AUTHENTICATION_KEY) ||
             this._settings.get_boolean(UNIFIED_AUTHENTICATION_KEY)) &&
             this._serviceIsAvailable(UNIFIED_AUTH_SERVICE_NAME))
            return UNIFIED_AUTH_SERVICE_NAME;

        if (this._ubuntuSettings.get_boolean(UBUNTU_AUTHD_AUTHENTICATION_KEY) &&
            this._serviceIsAvailable(UNIFIED_AUTH_SERVICE_NAME))
            return UNIFIED_AUTH_SERVICE_NAME;

        const definition = DiscreteServiceMechanismDefinitions.find(
            def => this._isDiscreteServiceEnabled(def.setting));
        return definition?.serviceName ?? null;
    }

    _updateDefaultService() {
        const oldDefaultService = this._defaultService;
        this._defaultService = this._getDetectedDefaultService();

        if (this._unavailableServices.has(this._defaultService))
            this._defaultService = null;

        if (!this._defaultService) {
            log('no authentication service is enabled, using password authentication');
            this._defaultService = PASSWORD_SERVICE_NAME;
        }

        if (oldDefaultService &&
            oldDefaultService !== this._defaultService &&
            this._activeServices.has(oldDefaultService))
            this._cancelAndReset();
    }

    async _startService(serviceName) {
        this._activeServices.add(serviceName);
        this._hold.acquire();
        try {
            this._activeServices.add(serviceName);
            if (this._userName) {
                await this._userVerifier.call_begin_verification_for_user(
                    serviceName, this._userName, this._cancellable);
            } else {
                await this._userVerifier.call_begin_verification(
                    serviceName, this._cancellable);
            }
        } catch (e) {
            this._activeServices.delete(serviceName);
            if (this.serviceIsForeground(serviceName) &&
                Gio.DBusError.is_remote_error(e) &&
                Gio.DBusError.get_remote_error(e) === 'org.gnome.DisplayManager.SessionWorker.Error.ServiceUnavailable') {
                this._unavailableServices.add(serviceName);
                this._updateDefaultService();
                this._beginVerification();
                return;
            }
            if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                return;
            if (!this.serviceIsForeground(serviceName)) {
                logError(e,
                    `Failed to start ${serviceName} for ${this._userName}`);
                this._hold.release();
                return;
            }
            this._reportInitError(
                this._userName
                    ? `Failed to start ${serviceName} verification for user`
                    : `Failed to start ${serviceName} verification`,
                e, serviceName);
            return;
        }
        this._hold.release();
    }

    async _maybeStartFingerprintVerification() {
        if (this._userName &&
            this._fingerprintReaderType !== FingerprintReaderType.NONE &&
            !this.serviceIsForeground(FINGERPRINT_SERVICE_NAME))
            await this._startService(FINGERPRINT_SERVICE_NAME);
    }

    _startBackgroundServices() {
        this._maybeStartFingerprintVerification().catch(logError);
    }

    _startMechanismFromUnifiedService(mechanism) {
        const unifiedService = this._unifiedAuthServices.get(mechanism.protocol);
        if (!unifiedService) {
            logError(new Error(`No unified services found for protocol ${mechanism.protocol}`));
            return;
        }

        if (unifiedService.handleMechanism(mechanism))
            return;

        const roleHandlers = {
            [WEB_LOGIN_ROLE_NAME]: this._startWebLogin,
            [PASSWORD_ROLE_NAME]: this._startQuestionLogin,
            [Const.PLAIN_TEXT_ROLE_NAME]: this._startQuestionLogin,
            [Const.CHOICE_LIST_ROLE_NAME]: this._startChoiceListSelection,
            [Const.MESSAGE_ROLE_NAME]: this._startMessageLogin,
        };

        const handler = roleHandlers[mechanism.role];
        if (handler)
            handler.call(this, mechanism.serviceName, mechanism.id);
    }

    _shouldStartBackgroundService(serviceName) {
        if (!this._userName)
            return false;

        if (this.serviceIsForeground(serviceName))
            return false;

        if (this._activeServices.has(serviceName))
            return false;

        if (this._unavailableServices.has(serviceName))
            return false;

        if (serviceName === FINGERPRINT_SERVICE_NAME)
            return this._fingerprintReaderType !== FingerprintReaderType.NONE;

        return true;
    }

    _onMechanismsListChanged(serviceName, mechanismsList) {
        /* Start all non-selectable (background) mechanisms and the foreground
         * mechanism. Note for discrete auth (e.g. not the non-unified services),
         * the foreground mechanism is already started explicitly in beginVerification
         */
        if (this._foregroundMechanism?.serviceName === UNIFIED_AUTH_SERVICE_NAME)
            this._startMechanismFromUnifiedService(this._foregroundMechanism);

        if (this._unavailableServices.has(serviceName))
            return;

        for (const mechanism of mechanismsList) {
            /* If it's selectable we wait until it's selected to start it
             */
            if (mechanism.selectable)
                continue;

            if (this._shouldStartBackgroundService(serviceName))
                this._startService(serviceName);

            if (serviceName === UNIFIED_AUTH_SERVICE_NAME)
                this._startMechanismFromUnifiedService(mechanism);
        }
    }

    _beginVerification() {
        const foregroundService = this._getForegroundService();

        if (this._mechanismsListChangedSignalId)
            this.disconnect(this._mechanismsListChangedSignalId);
        this._mechanismsListChangedSignalId =
            this.connect('mechanisms-list-changed',
                (_, ...args) => this._onMechanismsListChanged(...args));

        this._startService(foregroundService);
        this._startBackgroundServices();
        this._generateMechanismsFromDiscreteServices();
    }

    _onChoiceListQuery(client, serviceName, promptMessage, list) {
        this.emit(`service-request::${serviceName}`);

        if (!this.serviceIsForeground(serviceName))
            return;

        this.emit('show-choice-list', serviceName, promptMessage, list.deepUnpack());
    }

    _startChoiceListSelection(serviceName, mechanismId) {
        const mechanisms = this._publishedMechanisms.get(serviceName);

        if (!mechanisms) {
            logError(new Error(`Impossible to find mechanisms for ${serviceName}`));
            return;
        }

        const mechanism = mechanisms[mechanismId];
        if (!mechanism) {
            logError(new Error(`Impossible to find mechanism for ${mechanismId}`));
            return;
        }

        const {prompt, choices, role} = mechanism;

        this._pendingMechanisms.set(role, mechanismId);
        this.emit('show-choice-list', serviceName, prompt, choices);
    }

    _startQuestionLogin(serviceName, mechanismId) {
        const mechanisms = this._publishedMechanisms.get(serviceName);

        if (!mechanisms)
            return;

        if (!mechanisms[mechanismId])
            return;

        const {prompt, role} = mechanisms[mechanismId];
        const secret = role !== Const.PLAIN_TEXT_ROLE_NAME;

        this._pendingMechanisms.set(role, mechanismId);
        this.emit('ask-question', serviceName, prompt, secret);
    }

    _startMessageLogin(serviceName, mechanismId) {
        const mechanisms = this._publishedMechanisms.get(serviceName);

        if (!mechanisms)
            return;

        if (!mechanisms[mechanismId])
            return;

        const {prompt, role} = mechanisms[mechanismId];

        this._pendingMechanisms.set(role, mechanismId);
        this.emit('show-waiting-message', serviceName, prompt);
    }

    _replyWithAuthSelectionResponse(serviceName, role, response) {
        const mechanism = this._getPendingMechanismForRole(role);
        if (!mechanism)
            return;

        this._pendingMechanisms.delete(role);
        const authService = this._unifiedAuthServices.get(mechanism.protocol);

        if (!authService)
            throw new Error(`No authentication service for protocol ${mechanism.protocol}`);

        if (authService.handleAuthSelectionResponse(mechanism, role, response))
            return;

        const reply = authService.getProtocolResponse(mechanism, role, response);
        authService.sendProtocolResponse(reply);
    }

    _startWebLogin(serviceName, mechanismId) {
        const mechanisms = this._publishedMechanisms.get(serviceName);

        if (!mechanisms) {
            logError(new Error(`No mechanisms found for ${serviceName}`));
            return;
        }

        if (!mechanisms[mechanismId]) {
            logError(new Error(`No mechanisms found for ID ${mechanismId}`));
            return;
        }

        const {init_prompt: initPrompt, link_prompt: linkPrompt, uri, code, role, timeout} = mechanisms[mechanismId];

        if (!linkPrompt || !uri)
            return;

        if (timeout) {
            this._webLoginTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout * 1000,
                () => {
                    this.emit('web-login-time-out', serviceName);
                    this._webLoginTimeoutId = 0;
                    return GLib.SOURCE_REMOVE;
                });
        }

        this._pendingMechanisms.set(role, mechanismId);
        this.emit('web-login', serviceName, initPrompt, linkPrompt, uri, code);
    }

    _clearWebLoginTimeout() {
        if (this._webLoginTimeoutId) {
            GLib.source_remove(this._webLoginTimeoutId);
            this._webLoginTimeoutId = 0;
        }
    }

    _getPendingMechanismForRole(role) {
        const mechanismId = this._pendingMechanisms.get(role);
        if (!mechanismId)
            return null;

        return [...this._publishedMechanisms.values()].find(m => m[mechanismId])?.[mechanismId];
    }

    webLoginDone(serviceName) {
        this._clearWebLoginTimeout();

        const mechanism = this._getPendingMechanismForRole(WEB_LOGIN_ROLE_NAME);
        const authService = this._unifiedAuthServices.get(mechanism?.protocol);
        if (!authService)
            return;

        this._replyWithAuthSelectionResponse(serviceName, WEB_LOGIN_ROLE_NAME, {});
    }

    _filterAuthMechanisms(mechanismsList, filterFunc) {
        return mechanismsList.filter(mechanism => {
            const mapping = DiscreteServiceMechanismDefinitions.find(m =>
                m.role === mechanism.role);

            if (!mapping)
                return true;

            if (!mapping.selectable) {
                if (filterFunc)
                    filterFunc(mechanism);
                return false;
            }

            const enabled = this._settings.get_boolean(mapping.setting);

            if (!enabled && filterFunc)
                filterFunc(mechanism);

            return enabled;
        });
    }

    _onCustomJSONRequest(client, serviceName, protocol, version, json) {
        const authService = this._unifiedAuthServices.get(protocol);
        if (!authService) {
            console.log(`Got a request using protocol ${protocol} v${version} but it was ignored`);

            this._userVerifierCustomJSON.call_report_error(serviceName,
                `Unsupported Protocol: ${protocol} ${version}`, this._cancellable, null);
            return;
        }

        const id = authService.connect('protocol-request-handled', (_, reply) => {
            authService.disconnect(id);
            this._replyWithJSON(serviceName, JSON.stringify(reply)).catch(logError);
        });

        try {
            authService.handleProtocolRequest(protocol, version, json);
        } catch (e) {
            logError(e);

            this._userVerifierCustomJSON?.call_report_error(serviceName,
                e.toString(), this._cancellable, null);
        }
    }

    _onInfo(client, serviceName, info) {
        this.emit(`service-request::${serviceName}`);

        if (this.serviceIsForeground(serviceName)) {
            this._queueMessage(serviceName, info, MessageType.INFO);
        } else if (this.serviceIsFingerprint(serviceName)) {
            // We don't show fingerprint messages directly since it's
            // not the main auth service. Instead we use the messages
            // as a cue to display our own message.
            if (this._fingerprintReaderType === FingerprintReaderType.SWIPE) {
                // Translators: this message is shown below the password entry field
                // to indicate the user can swipe their finger on the fingerprint reader
                this._queueMessage(serviceName, _('(or swipe finger across reader)'),
                    MessageType.HINT);
            } else {
                // Translators: this message is shown below the password entry field
                // to indicate the user can place their finger on the fingerprint reader instead
                this._queueMessage(serviceName, _('(or place finger on reader)'),
                    MessageType.HINT);
            }
        }
    }

    _onProblem(client, serviceName, problem) {
        this.emit(`service-request::${serviceName}`);

        const isFingerprint = this.serviceIsFingerprint(serviceName);

        if (!this.serviceIsForeground(serviceName) && !isFingerprint)
            return;

        this._queuePriorityMessage(serviceName, problem, MessageType.ERROR);

        if (isFingerprint) {
            // pam_fprintd allows the user to retry multiple (maybe even infinite!
            // times before failing the authentication conversation.
            // We don't want this behavior to bypass the max-tries setting the user has set,
            // so we count the problem messages to know how many times the user has failed.
            // Once we hit the max number of failures we allow, it's time to failure the
            // conversation from our side. We can't do that right away, however, because
            // we may drop pending messages coming from pam_fprintd. In order to make sure
            // the user sees everything, we queue the failure up to get handled in the
            // near future, after we've finished up the current round of messages.
            this._failCounter++;

            if (!this._canRetry()) {
                if (this._fingerprintFailedId)
                    GLib.source_remove(this._fingerprintFailedId);

                const cancellable = this._cancellable;
                this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
                    FINGERPRINT_ERROR_TIMEOUT_WAIT, () => {
                        this._fingerprintFailedId = 0;
                        if (!cancellable.is_cancelled())
                            this._verificationFailed(serviceName, false);
                        return GLib.SOURCE_REMOVE;
                    });
            }
        }
    }

    _onInfoQuery(client, serviceName, question) {
        this.emit(`service-request::${serviceName}`);

        if (!this.serviceIsForeground(serviceName))
            return;

        this.emit('ask-question', serviceName, question, false);
    }

    _onSecretInfoQuery(client, serviceName, secretQuestion) {
        this.emit(`service-request::${serviceName}`);

        if (!this.serviceIsForeground(serviceName))
            return;

        let token = null;
        if (this._credentialManagers[serviceName])
            token = this._credentialManagers[serviceName].token;

        if (token) {
            this.answerQuery(serviceName, token);
            return;
        }

        this.emit('ask-question', serviceName, secretQuestion, true);
    }

    _generateMechanismsFromDiscreteServices() {
        // Unified auth doesn't support all authentication mechanisms (e.g. fingerprint) and also sometimes unified auth
        // isn't available at all. Fill in the gaps in coverage with mechanisms synthesized from non-unified authentication
        // services.
        const unifiedAuthAvailable = this._activeServices.has(UNIFIED_AUTH_SERVICE_NAME) && !this._unavailableServices.has(UNIFIED_AUTH_SERVICE_NAME);
        for (const definition of DiscreteServiceMechanismDefinitions) {
            const enabled = this._isDiscreteServiceEnabled(definition);
            const available = this._activeServices.has(definition.serviceName) &&
                !this._unavailableServices.has(definition.serviceName);
            const supportedByUnifiedAuth = unifiedAuthAvailable &&
                UnifiedMechanism.SUPPORTED_ROLES.includes(definition.role);

            const mechanismsList = [];
            if (enabled && available && !supportedByUnifiedAuth) {
                mechanismsList.push({
                    id: definition.mechanismId,
                    name: definition.mechanismName,
                    role: definition.role,
                    selectable: definition.selectable,
                });
            }
            this.emit('mechanisms-list-changed', definition.serviceName, mechanismsList);
        }
    }

    _onReset() {
        // Clear previous attempts to authenticate
        this._failCounter = 0;
        this._activeServices.clear();
        this._unavailableServices.clear();
        this._activeServices.clear();
        [...this._unifiedAuthServices.values()].forEach(s => s.reset());
        if (this._mechanismsListChangedSignalId) {
            this.disconnect(this._mechanismsListChangedSignalId);
            this._mechanismsListChangedSignalId = 0;
        }
        this._updateDefaultService();

        this.emit('reset');
    }

    _onVerificationComplete() {
        this.emit('verification-complete');
    }

    _cancelAndReset() {
        this.cancel();
        this._onReset();
    }

    _retry(serviceName) {
        this._hold = new Batch.Hold();
        this._connectSignals();
        this._startService(serviceName);
    }

    _canRetry() {
        return this._userName &&
            (this._reauthOnly || this._failCounter < this.allowedFailures);
    }

    async _verificationFailed(serviceName, shouldRetry) {
        if (serviceName === FINGERPRINT_SERVICE_NAME) {
            if (this._fingerprintFailedId)
                GLib.source_remove(this._fingerprintFailedId);
        }

        // For Not Listed / enterprise logins, immediately reset
        // the dialog
        // Otherwise, when in login mode we allow ALLOWED_FAILURES attempts.
        // After that, we go back to the welcome screen.
        this._filterServiceMessages(serviceName, MessageType.ERROR);

        const doneTrying = !shouldRetry || !this._canRetry();

        this.emit('verification-failed', serviceName, !doneTrying);

        if (!this._userVerifier)
            return;

        try {
            if (doneTrying) {
                this._disconnectSignals();
                await this._handlePendingMessages();
                this._cancelAndReset();
            } else {
                await this._handlePendingMessages();
                this._retry(serviceName);
            }
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                logError(e);
        }
    }

    _handlePendingMessages() {
        if (!this.hasPendingMessage)
            return Promise.resolve();

        const cancellable = this._cancellable;
        return new Promise((resolve, reject) => {
            let signalId = this.connect('no-more-messages', () => {
                this.disconnect(signalId);
                if (cancellable.is_cancelled())
                    reject(new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, 'Operation was cancelled'));
                else
                    resolve();
            });
        });
    }

    _onServiceUnavailable(_client, serviceName, errorMessage) {
        this._unavailableServices.add(serviceName);

        if (serviceName === UNIFIED_AUTH_SERVICE_NAME)
            this._generateMechanismsFromDiscreteServices();

        if (!errorMessage)
            return;

        if (this.serviceIsForeground(serviceName) || this.serviceIsFingerprint(serviceName))
            this._queueMessage(serviceName, errorMessage, MessageType.ERROR);
    }

    _onConversationStarted(client, serviceName) {
        this._activeServices.add(serviceName);
    }

    _onConversationStopped(client, serviceName) {
        this._activeServices.delete(serviceName);
        this.emit('mechanisms-list-changed', serviceName, []);

        // If the login failed with the preauthenticated oVirt credentials
        // then discard the credentials and revert to default authentication
        // mechanism.
        let foregroundService = Object.keys(this._credentialManagers).find(service =>
            this.serviceIsForeground(service));
        if (foregroundService) {
            this._credentialManagers[foregroundService].token = null;
            this.setForegroundService(null);
            this._verificationFailed(serviceName, false);
            return;
        }

        this._filterServiceMessages(serviceName, MessageType.ERROR);

        if (this._unavailableServices.has(serviceName))
            return;

        // if the password service fails, then cancel everything.
        // But if, e.g., fingerprint fails, still give
        // password authentication a chance to succeed
        if (this.serviceIsForeground(serviceName))
            this._failCounter++;

        this._verificationFailed(serviceName, true);
    }
}
(uuay)signalTracker.jszimport GObject from 'gi://GObject';

const destroyableTypes = [];

/**
 * @private
 * @param {object} obj - an object
 * @returns {bool} - true if obj has a 'destroy' GObject signal
 */
function _hasDestroySignal(obj) {
    return destroyableTypes.some(type => obj instanceof type);
}

export const TransientSignalHolder = GObject.registerClass(
class TransientSignalHolder extends GObject.Object {
    static [GObject.signals] = {
        'destroy': {},
    };

    constructor(owner) {
        super();

        if (_hasDestroySignal(owner))
            owner.connectObject('destroy', () => this.destroy(), this);
    }

    destroy() {
        this.emit('destroy');
    }
});
registerDestroyableType(TransientSignalHolder);

class SignalManager {
    /**
     * @returns {SignalManager} - the SignalManager singleton
     */
    static getDefault() {
        if (!this._singleton)
            this._singleton = new SignalManager();
        return this._singleton;
    }

    constructor() {
        this._signalTrackers = new Map();

        global.connect_after('shutdown', () => {
            [...this._signalTrackers.values()].forEach(
                tracker => tracker.destroy());
            this._signalTrackers.clear();
        });
    }

    /**
     * @param {object} obj - object to get signal tracker for
     * @returns {SignalTracker} - the signal tracker for object
     */
    getSignalTracker(obj) {
        let signalTracker = this._signalTrackers.get(obj);
        if (signalTracker === undefined) {
            signalTracker = new SignalTracker(obj);
            this._signalTrackers.set(obj, signalTracker);
        }
        return signalTracker;
    }

    /**
     * @param {object} obj - object to get signal tracker for
     * @returns {?SignalTracker} - the signal tracker for object if it exists
     */
    maybeGetSignalTracker(obj) {
        return this._signalTrackers.get(obj) ?? null;
    }

    /*
     * @param {object} obj - object to remove signal tracker for
     * @returns {void}
     */
    removeSignalTracker(obj) {
        this._signalTrackers.delete(obj);
    }
}

class SignalTracker {
    /**
     * @param {object=} owner - object that owns the tracker
     */
    constructor(owner) {
        if (_hasDestroySignal(owner))
            this._ownerDestroyId = owner.connect_after('destroy', () => this.clear());

        this._owner = owner;
        this._map = new Map();
    }

    /**
     * @typedef SignalData
     * @property {number[]} ownerSignals - a list of handler IDs
     * @property {number} destroyId - destroy handler ID of tracked object
     */

    /**
     * @private
     * @param {object} obj - a tracked object
     * @returns {SignalData} - signal data for object
     */
    _getSignalData(obj) {
        let data = this._map.get(obj);
        if (data === undefined) {
            data = {ownerSignals: [], destroyId: 0};
            this._map.set(obj, data);
        }
        return data;
    }

    /**
     * @private
     * @param {GObject.Object} obj - tracked widget
     */
    _trackDestroy(obj) {
        const signalData = this._getSignalData(obj);
        if (signalData.destroyId)
            return;
        signalData.destroyId = obj.connect_after('destroy', () => this.untrack(obj));
    }

    _disconnectSignalForProto(proto, obj, id) {
        proto['disconnect'].call(obj, id);
    }

    _getObjectProto(obj) {
        return obj instanceof GObject.Object
            ? GObject.Object.prototype
            : Object.getPrototypeOf(obj);
    }

    _disconnectSignal(obj, id) {
        this._disconnectSignalForProto(this._getObjectProto(obj), obj, id);
    }

    _removeTracker() {
        if (this._ownerDestroyId)
            this._disconnectSignal(this._owner, this._ownerDestroyId);

        SignalManager.getDefault().removeSignalTracker(this._owner);

        delete this._ownerDestroyId;
        delete this._owner;
    }

    /**
     * @param {object} obj - tracked object
     * @param {...number} handlerIds - tracked handler IDs
     * @returns {void}
     */
    track(obj, ...handlerIds) {
        if (_hasDestroySignal(obj))
            this._trackDestroy(obj);

        this._getSignalData(obj).ownerSignals.push(...handlerIds);
    }

    /**
     * @param {object} obj - tracked object instance
     * @returns {void}
     */
    untrack(obj) {
        const {ownerSignals, destroyId} = this._getSignalData(obj);
        this._map.delete(obj);

        const ownerProto = this._getObjectProto(this._owner);
        ownerSignals.forEach(id =>
            this._disconnectSignalForProto(ownerProto, this._owner, id));
        if (destroyId)
            this._disconnectSignal(obj, destroyId);

        if (this._map.size === 0)
            this._removeTracker();
    }

    /**
     * @returns {void}
     */
    clear() {
        this._map.forEach((_, obj) => this.untrack(obj));
    }

    /**
     * @returns {void}
     */
    destroy() {
        this.clear();
        this._removeTracker();
    }
}

/**
 * Connect one or more signals, and associate the handlers
 * with a tracked object.
 *
 * All handlers for a particular object can be disconnected
 * by calling disconnectObject(). If object is a {Clutter.widget},
 * this is done automatically when the widget is destroyed.
 *
 * @param {object} thisObj - the emitter object
 * @param {...any} args - a sequence of signal-name/handler pairs
 * with an optional flags value, followed by an object to track
 * @returns {void}
 */
export function connectObject(thisObj, ...args) {
    const getParams = argArray => {
        const [signalName, handler, arg, ...rest] = argArray;
        if (typeof arg !== 'number')
            return [signalName, handler, 0, arg, ...rest];

        const flags = arg;
        let flagsMask = 0;
        Object.values(GObject.ConnectFlags).forEach(v => (flagsMask |= v));
        if (!(flags & flagsMask))
            throw new Error(`Invalid flag value ${flags}`);
        if (flags & GObject.ConnectFlags.SWAPPED)
            throw new Error('Swapped signals are not supported');
        return [signalName, handler, flags, ...rest];
    };

    const connectSignal = (emitter, signalName, handler, flags) => {
        const isGObject = emitter instanceof GObject.Object;
        const func = (flags & GObject.ConnectFlags.AFTER) && isGObject
            ? 'connect_after'
            : 'connect';
        const emitterProto = isGObject
            ? GObject.Object.prototype
            : Object.getPrototypeOf(emitter);
        return emitterProto[func].call(emitter, signalName, handler);
    };

    const signalIds = [];
    while (args.length > 1) {
        const [signalName, handler, flags, ...rest] = getParams(args);
        signalIds.push(connectSignal(thisObj, signalName, handler, flags));
        args = rest;
    }

    const obj = args.at(0) ?? globalThis;
    const tracker = SignalManager.getDefault().getSignalTracker(thisObj);
    tracker.track(obj, ...signalIds);
}

/**
 * Disconnect all signals that were connected for
 * the specified tracked object
 *
 * @param {object} thisObj - the emitter object
 * @param {object} obj - the tracked object
 * @returns {void}
 */
export function disconnectObject(thisObj, obj) {
    SignalManager.getDefault().maybeGetSignalTracker(thisObj)?.untrack(obj);
}

/**
 * Register a GObject type as having a 'destroy' signal
 * that should disconnect all handlers
 *
 * @param {GObject.Type} gtype - a GObject type
 */
export function registerDestroyableType(gtype) {
    if (!GObject.type_is_a(gtype, GObject.Object))
        throw new Error(`${gtype} is not a GObject subclass`);

    if (!GObject.signal_lookup('destroy', gtype))
        throw new Error(`${gtype} does not have a destroy signal`);

    destroyableTypes.push(gtype);
}
(uuay)rfkill.jsN// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';

import {QuickToggle, SystemIndicator} from '../quickSettings.js';

import {loadInterfaceXML} from '../../misc/fileUtils.js';

const BUS_NAME = 'org.gnome.SettingsDaemon.Rfkill';
const OBJECT_PATH = '/org/gnome/SettingsDaemon/Rfkill';

const RfkillManagerInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Rfkill');
const rfkillManagerInfo = Gio.DBusInterfaceInfo.new_for_xml(RfkillManagerInterface);

const RfkillManager = GObject.registerClass({
    Properties: {
        'airplane-mode': GObject.ParamSpec.boolean(
            'airplane-mode', '', '',
            GObject.ParamFlags.READWRITE,
            false),
        'hw-airplane-mode': GObject.ParamSpec.boolean(
            'hw-airplane-mode', '', '',
            GObject.ParamFlags.READABLE,
            false),
        'show-airplane-mode': GObject.ParamSpec.boolean(
            'show-airplane-mode', '', '',
            GObject.ParamFlags.READABLE,
            false),
    },
}, class RfkillManager extends GObject.Object {
    constructor() {
        super();

        this._proxy = new Gio.DBusProxy({
            g_connection: Gio.DBus.session,
            g_name: BUS_NAME,
            g_object_path: OBJECT_PATH,
            g_interface_name: rfkillManagerInfo.name,
            g_interface_info: rfkillManagerInfo,
        });
        this._proxy.connect('g-properties-changed', this._changed.bind(this));
        this._proxy.init_async(GLib.PRIORITY_DEFAULT, null)
            .catch(e => console.error(e.message));
    }

    /* eslint-disable camelcase */
    get airplane_mode() {
        return this._proxy.AirplaneMode;
    }

    set airplane_mode(v) {
        this._proxy.AirplaneMode = v;
    }

    get hw_airplane_mode() {
        return this._proxy.HardwareAirplaneMode;
    }

    get show_airplane_mode() {
        return this._proxy.HasAirplaneMode && this._proxy.ShouldShowAirplaneMode;
    }
    /* eslint-enable camelcase */

    _changed(proxy, properties) {
        for (const prop in properties.deepUnpack()) {
            switch (prop) {
            case 'AirplaneMode':
                this.notify('airplane-mode');
                break;
            case 'HardwareAirplaneMode':
                this.notify('hw-airplane-mode');
                break;
            case 'HasAirplaneMode':
            case 'ShouldShowAirplaneMode':
                this.notify('show-airplane-mode');
                break;
            }
        }
    }
});

let _manager;

/**
 * @returns {RfkillManager}
 */
export function getRfkillManager() {
    if (_manager != null)
        return _manager;

    _manager = new RfkillManager();
    return _manager;
}

const RfkillToggle = GObject.registerClass(
class RfkillToggle extends QuickToggle {
    _init() {
        super._init({
            title: _('Airplane Mode'),
            iconName: 'airplane-mode-symbolic',
        });

        this._manager = getRfkillManager();
        this._manager.bind_property('show-airplane-mode',
            this, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        this._manager.bind_property('airplane-mode',
            this, 'checked',
            GObject.BindingFlags.SYNC_CREATE);

        this.connect('clicked',
            () => (this._manager.airplaneMode = !this._manager.airplaneMode));
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'airplane-mode-symbolic';

        this._rfkillToggle = new RfkillToggle();
        this._rfkillToggle.connectObject(
            'notify::visible', () => this._sync(),
            'notify::checked', () => this._sync(),
            this);
        this.quickSettingsItems.push(this._rfkillToggle);

        this._sync();
    }

    _sync() {
        // Only show indicator when airplane mode is on
        const {visible, checked} = this._rfkillToggle;
        this._indicator.visible = visible && checked;
    }
});
(uuay)backgroundApps.js� import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Main from '../main.js';
import * as PopupMenu from '../popupMenu.js';
import * as Util from '../../misc/util.js';

import {Spinner} from '../animation.js';
import {QuickToggle, SystemIndicator} from '../quickSettings.js';
import {loadInterfaceXML} from '../../misc/dbusUtils.js';

const DBUS_NAME = 'org.freedesktop.background.Monitor';
const DBUS_OBJECT_PATH = '/org/freedesktop/background/monitor';

const SPINNER_TIMEOUT = 5; // seconds

const BackgroundMonitorIface = loadInterfaceXML('org.freedesktop.background.Monitor');
const BackgroundMonitorProxy = Gio.DBusProxy.makeProxyWrapper(BackgroundMonitorIface);

Gio._promisify(Gio.DBusConnection.prototype, 'call');

const BackgroundAppMenuItem = GObject.registerClass({
    Properties: {
        'app': GObject.ParamSpec.object('app', '', '',
            GObject.ParamFlags.READWRITE,
            Shell.App),
        'message': GObject.ParamSpec.string('message', '', '',
            GObject.ParamFlags.READWRITE,
            null),
    },
}, class BackgroundAppMenuItem extends PopupMenu.PopupImageMenuItem {
    _init(app, params = {}) {
        const message = params.message;
        delete params.message;

        super._init(app.get_name(), app.get_icon(), {
            ...params,
        });

        this.set({message});

        this.add_style_class_name('background-app-item');
        this.label.add_style_class_name('title');

        this.app = app;

        const box = new St.BoxLayout({
            vertical: true,
            x_expand: true,
            x_align: Clutter.ActorAlign.START,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(box);

        this.remove_child(this.label);
        box.add_child(this.label);

        const messageLabel = new St.Label({
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
            style_class: 'subtitle',
        });
        box.add_child(messageLabel);

        this.bind_property('message',
            messageLabel, 'text', GObject.BindingFlags.SYNC_CREATE);
        this.bind_property_full('message',
            messageLabel, 'visible', GObject.BindingFlags.SYNC_CREATE,
            (bind, source) => [true, source !== null],
            null);

        this.set_child_above_sibling(this._ornamentIcon, null);

        this._spinner = new Spinner(16, {hideOnStop: true});
        this._spinner.add_style_class_name('spinner');
        this.add_child(this._spinner);

        const closeButton = new St.Button({
            iconName: 'window-close-symbolic',
            styleClass: 'icon-button',
            x_expand: true,
            y_expand: false,
            x_align: Clutter.ActorAlign.END,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(closeButton);

        this._spinner.bind_property('visible',
            closeButton, 'visible',
            GObject.BindingFlags.INVERT_BOOLEAN);

        closeButton.connect('clicked', () => this._quitApp().catch(logError));

        this.connect('activate', () => {
            Main.overview.hide();
            Main.panel.closeQuickSettings();
            this.app.activate();
        });

        this.connect('destroy', () => this._onDestroy());
    }

    _onDestroy() {
        if (this._spinnerTimeoutId)
            GLib.source_remove(this._spinnerTimeoutId);
        delete this._spinnerTimeoutId;
    }

    async _quitApp() {
        this._spinner.play();
        this._spinnerTimeoutId =
            GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, SPINNER_TIMEOUT,
                () => {
                    // Assume the quit request has failed, stop the spinner
                    this._spinner.stop();
                    delete this._spinnerTimeoutId;
                    return GLib.SOURCE_REMOVE;
                });

        try {
            await this.app.activate_action('quit', null, 0, -1, null);
        } catch (_error) {
            try {
                const appId = this.app.get_id().replace(/\.desktop$/, '');
                Util.trySpawn(['flatpak', 'kill', appId]);
            } catch (pidError) {
                logError(pidError, 'Failed to kill application');
            }
        }
    }
});

const BackgroundAppsToggle = GObject.registerClass(
class BackgroundAppsToggle extends QuickToggle {
    _init() {
        super._init({
            visible: false,
            hasMenu: true,
            // The background apps toggle looks like a flat menu, but doesn't
            // have a separate menu button. Fake it with an arrow icon.
            iconName: 'go-next-symbolic',
        });

        this.add_style_class_name('background-apps-quick-toggle');

        this._box.set_child_above_sibling(this._icon, null);

        this._appSystem = Shell.AppSystem.get_default();

        this.menu.setHeader(
            'background-app-ghost-symbolic',
            C_('title', 'Background Apps'));

        new BackgroundMonitorProxy(
            Gio.DBus.session,
            DBUS_NAME,
            DBUS_OBJECT_PATH,
            proxy => {
                this._proxy = proxy;
                proxy?.connect('g-properties-changed', () => this._sync());
                this._sync();
            },
            null,
            Gio.DBusProxyFlags.DO_NOT_AUTO_START);

        this._listTitle = new PopupMenu.PopupMenuItem(
            _('Apps known to be running without a window'),
            {reactive: false});
        this._listTitle.label.clutter_text.set({
            line_wrap: true,
        });
        this.menu.addMenuItem(this._listTitle);

        this._appsSection = new PopupMenu.PopupMenuSection();
        this.menu.addMenuItem(this._appsSection);

        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this.menu.addSettingsAction(_('App Settings'),
            'gnome-applications-panel.desktop');

        this.connect('popup-menu', () => this.menu.open());

        this.menu.connect('open-state-changed', () => this._syncVisibility());
        Main.sessionMode.connect('updated', () => this._syncVisibility());
    }

    _syncVisibility() {
        const {isLocked} = Main.sessionMode;
        const nBackgroundApps = this._proxy?.BackgroundApps?.length;
        // We cannot hide the quick toggle while the menu is open, otherwise
        // the menu position goes bogus. We can't show it in locked sessions
        // either
        this.visible = !isLocked && (this.menu.isOpen || nBackgroundApps > 0);
    }

    _sync() {
        this._syncVisibility();

        if (!this._proxy)
            return;

        const {BackgroundApps: backgroundApps} = this._proxy;

        this._appsSection.removeAll();

        const items = new Map();
        (backgroundApps ?? [])
            .map(backgroundApp => {
                const appId = backgroundApp.app_id.deepUnpack();
                const app = this._appSystem.lookup_app(`${appId}.desktop`);
                const message = backgroundApp.message?.deepUnpack();

                return {app, message};
            })
            .filter(item => !!item.app)
            .sort((a, b) => {
                return a.app.get_name().localeCompare(b.app.get_name());
            })
            .forEach(backgroundApp => {
                const {app, message} = backgroundApp;

                let item = items.get(app);
                if (!item) {
                    item = new BackgroundAppMenuItem(app);
                    items.set(app, item);
                    this._appsSection.addMenuItem(item);
                }

                if (message)
                    item.set({message});
            });

        const nBackgroundApps = items.size;
        this.title = nBackgroundApps === 0
            ? _('No Background Apps')
            : ngettext(
                '%d Background App',
                '%d Background Apps',
                nBackgroundApps).format(nBackgroundApps);
        this._listTitle.visible = nBackgroundApps > 0;
    }

    vfunc_clicked() {
        this.menu.open();
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this.quickSettingsItems.push(new BackgroundAppsToggle());
    }
});
(uuay)oVirt.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import * as Credential from './credentialManager.js';

export const SERVICE_NAME = 'gdm-ovirtcred';

const OVirtCredentialsIface = `
<node>
<interface name="org.ovirt.vdsm.Credentials">
<signal name="UserAuthenticated">
    <arg type="s" name="token"/>
</signal>
</interface>
</node>`;

const OVirtCredentialsInfo = Gio.DBusInterfaceInfo.new_for_xml(OVirtCredentialsIface);

let _oVirtCredentialsManager = null;

function OVirtCredentials() {
    var self = new Gio.DBusProxy({
        g_connection: Gio.DBus.system,
        g_interface_name: OVirtCredentialsInfo.name,
        g_interface_info: OVirtCredentialsInfo,
        g_name: 'org.ovirt.vdsm.Credentials',
        g_object_path: '/org/ovirt/vdsm/Credentials',
        g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
    });
    self.init(null);
    return self;
}

class OVirtCredentialsManager extends Credential.CredentialManager {
    constructor() {
        super(SERVICE_NAME);
        this._credentials = new OVirtCredentials();
        this._credentials.connectSignal('UserAuthenticated',
            (proxy, sender, [token]) => {
                this.token = token;
            });
    }
}

/**
 * @returns {OVirtCredentialsManager}
 */
export function getOVirtCredentialsManager() {
    if (!_oVirtCredentialsManager)
        _oVirtCredentialsManager = new OVirtCredentialsManager();

    return _oVirtCredentialsManager;
}
(uuay)backgroundMenu.jsH	// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import St from 'gi://St';

import * as BoxPointer from './boxpointer.js';
import * as PopupMenu from './popupMenu.js';

import * as Main from './main.js';

export class BackgroundMenu extends PopupMenu.PopupMenu {
    constructor(layoutManager) {
        super(layoutManager.dummyCursor, 0, St.Side.TOP);

        this.addSettingsAction(_('Change Background…'), 'gnome-background-panel.desktop');
        this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this.addSettingsAction(_('Display Settings'), 'gnome-display-panel.desktop');
        this.addSettingsAction(_('Settings'), 'org.gnome.Settings.desktop');

        this.actor.add_style_class_name('background-menu');

        layoutManager.uiGroup.add_child(this.actor);
        this.actor.hide();
    }
}

/**
 * @param {Meta.BackgroundActor} actor
 * @param {import('./layout.js').LayoutManager} layoutManager
 */
export function addBackgroundMenu(actor, layoutManager) {
    actor.reactive = true;
    actor._backgroundMenu = new BackgroundMenu(layoutManager);
    actor._backgroundManager = new PopupMenu.PopupMenuManager(actor);
    actor._backgroundManager.addMenu(actor._backgroundMenu);

    function openMenu(x, y) {
        Main.layoutManager.setDummyCursorGeometry(x, y, 0, 0);
        actor._backgroundMenu.open(BoxPointer.PopupAnimation.FULL);
    }

    let clickAction = new Clutter.ClickAction();
    clickAction.connect('long-press', (action, theActor, state) => {
        if (state === Clutter.LongPressState.QUERY) {
            return (action.get_button() === 0 ||
                     action.get_button() === 1) &&
                    !actor._backgroundMenu.isOpen;
        }
        if (state === Clutter.LongPressState.ACTIVATE) {
            let [x, y] = action.get_coords();
            openMenu(x, y);
            actor._backgroundManager.ignoreRelease();
        }
        return true;
    });
    clickAction.connect('clicked', action => {
        if (action.get_button() === 3) {
            let [x, y] = action.get_coords();
            openMenu(x, y);
        }
    });
    actor.add_action(clickAction);

    actor.connect('destroy', () => {
        actor._backgroundMenu.destroy();
        actor._backgroundMenu = null;
        actor._backgroundManager = null;
    });
}
(uuay)authNotification.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/*
 * Copyright 2024 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

import GObject from 'gi://GObject';
import * as Calendar from '../ui/calendar.js';
import * as MessageTray from '../ui/messageTray.js';
import * as Params from '../misc/params.js';

export const AuthNotificationSource = GObject.registerClass({
}, class AuthNotificationSource extends MessageTray.Source {
    constructor(params) {
        params = Params.parse(params, {
            title: _('Login Failed'),
            iconName: 'dialog-password-symbolic',
        });

        super(params);
    }

    createBanner(notification) {
        return new AuthNotificationBanner(notification);
    }
});

const AuthNotificationBanner = GObject.registerClass({
    Signals: {
        'done-displaying': {},
    },
}, class AuthNotificationBanner extends Calendar.NotificationMessage {
    _init(notification) {
        super._init(notification);

        this.can_focus = false;
        this.add_style_class_name('notification-banner');
        this.add_style_class_name('auth-notification-banner');

        this.notification.connectObject('activated', () => {
            // We hide all types of notifications once the user clicks on
            // them because the common outcome of clicking should be the
            // relevant window being brought forward and the user's
            // attention switching to the window.
            this.emit('done-displaying');
        }, this);
    }

    createBanner(notification) {
        return new AuthNotificationBanner(notification);
    }
});
(uuay)authMenuButton.jsv// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/*
 * Copyright 2024 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as BoxPointer from '../ui/boxpointer.js';
import * as Main from '../ui/main.js';
import * as Params from '../misc/params.js';
import * as PopupMenu from '../ui/popupMenu.js';

export const AuthMenuButton = GObject.registerClass({
    Signals: {'active-item-changed': {}},
}, class AuthMenuButton extends St.Bin {
    _init(params) {
        params = Params.parse(params, {
            title: '',
            iconName: '',
        });

        let button = new St.Button({
            style_class: 'login-dialog-button login-dialog-auth-menu-button',
            icon_name: params.iconName,
            reactive: true,
            track_hover: true,
            can_focus: true,
            accessible_name: params.title,
            accessible_role: Atk.Role.MENU,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });

        super._init({child: button});
        this._button = button;

        this._menu = new PopupMenu.PopupMenu(this._button, 0, St.Side.BOTTOM);
        this._menu.box.add_style_class_name('login-dialog-auth-menu-button-popup-menu-box');
        Main.uiGroup.add_child(this._menu.actor);
        this._menu.actor.hide();

        this._header = new St.Label({
            text: params.title,
            style_class: 'login-dialog-auth-menu-button-title',
            y_align: Clutter.ActorAlign.START,
            y_expand: true,
        });
        this._menu.box.add_child(this._header);

        this._menu.connect('open-state-changed', (menu, isOpen) => {
            if (isOpen)
                this._button.add_style_pseudo_class('active');
            else
                this._button.remove_style_pseudo_class('active');
        });

        this._manager = new PopupMenu.PopupMenuManager(this._button,
            {actionMode: Shell.ActionMode.NONE});
        this._manager.addMenu(this._menu);

        this._button.connect('clicked', () => this._menu.toggle());

        this._items = new Map();
        this._activeItem = null;
        this.updateSensitivity(true);
    }

    _getMenuItem(item) {
        if (!item)
            return null;

        return this._items.get(JSON.stringify(item));
    }

    updateSensitivity(sensitive) {
        this._sensitive = sensitive;

        if (this._items.size <= 1)
            sensitive = false;

        this._button.reactive = sensitive;
        this._button.can_focus = sensitive;
        this.opacity = sensitive ? 255 : 0;
        this._menu.close(BoxPointer.PopupAnimation.NONE);
    }

    _updateOrnament() {
        for (const menuItem of this._items.values())
            menuItem.setOrnament(PopupMenu.Ornament.NO_DOT);

        const activeMenuItem = this._getMenuItem(this._activeItem);

        if (activeMenuItem)
            activeMenuItem.setOrnament(PopupMenu.Ornament.DOT);
    }

    _findItems(searchCriteria) {
        let items = [];
        for (const itemKey of this._items.keys()) {
            const item = JSON.parse(itemKey);

            let criteriaMismatch = false;
            for (const key of Object.keys(searchCriteria)) {
                if (!searchCriteria[key])
                    continue;

                if (item[key] === searchCriteria[key])
                    continue;

                criteriaMismatch = true;
                break;
            }

            if (criteriaMismatch)
                continue;

            items.push(itemKey);
        }

        return items;
    }

    clearItems(searchCriteria) {
        if (!searchCriteria)
            searchCriteria = {};

        const activeMenuItem = this._getMenuItem(this._activeItem);
        const itemKeys = this._findItems(searchCriteria);
        for (const itemKey of itemKeys) {
            const menuItem = this._items.get(itemKey);

            if (activeMenuItem === menuItem)
                this._activeItem = null;

            menuItem.destroy();
            this._items.delete(itemKey);
        }
    }

    addItem(item) {
        let menuItem = this._getMenuItem(item);

        if (menuItem)
            throw new Error(`Duplicate item ${JSON.stringify(item)}`);

        if (!item.name)
            throw new Error(`item ${JSON.stringify(item)} lacks name`);

        menuItem = new PopupMenu.PopupMenuItem(item.name, {
            style_class: 'login-dialog-auth-menu-button-item',
        });
        menuItem.setOrnament(PopupMenu.Ornament.NO_DOT);
        menuItem.connect('activate', () => {
            this.setActiveItem(item);
        });

        this._menu.addMenuItem(menuItem);
        this._items.set(JSON.stringify(item), menuItem);
        this.updateSensitivity(this._sensitive);
    }

    _resolveItem(searchCriteria) {
        const itemKeys = this._findItems(searchCriteria);

        if (!itemKeys.length)
            throw new Error(`Unknown item ${JSON.stringify(searchCriteria)}`);

        if (itemKeys.length > 1)
            throw new Error(`Matched multiple items with criteria ${JSON.stringify(searchCriteria)}`);

        const item = JSON.parse(itemKeys[0]);
        const menuItem = this._items.get(itemKeys[0]);
        return {item, menuItem};
    }

    setActiveItem(searchCriteria) {
        const {item, menuItem} = this._resolveItem(searchCriteria);
        const activeMenuItem = this._getMenuItem(this._activeItem);

        if (menuItem === activeMenuItem)
            return;

        this._activeItem = item;
        this._updateOrnament();
        this.emit('active-item-changed');
    }

    getActiveItem() {
        return this._activeItem;
    }

    close() {
        this._menu.close();
    }
});
(uuay)workspaceAnimation.jsF// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Background from './background.js';
import * as Layout from './layout.js';
import * as SwipeTracker from './swipeTracker.js';
import * as Util from '../misc/util.js';

import * as Main from './main.js';

const WINDOW_ANIMATION_TIME = 250;
export const WORKSPACE_SPACING = 100;

export const WorkspaceGroup = GObject.registerClass(
class WorkspaceGroup extends Clutter.Actor {
    _init(workspace, monitor, movingWindow) {
        super._init({
            width: monitor.width,
            height: monitor.height,
            clip_to_allocation: true,
        });

        this._workspace = workspace;
        this._monitor = monitor;
        this._movingWindow = movingWindow;
        this._windowRecords = [];

        if (this._workspace) {
            this._background = new Meta.BackgroundGroup();

            this.add_child(this._background);

            this._bgManager = new Background.BackgroundManager({
                container: this._background,
                monitorIndex: this._monitor.index,
                controlPosition: false,
            });
            this._createDesktopWindows();
        }

        this._createWindows();

        this.connect('destroy', this._onDestroy.bind(this));
        global.display.connectObject('restacked',
            this._syncStacking.bind(this), this);
    }

    get workspace() {
        return this._workspace;
    }

    _shouldShowWindow(window) {
        if (!window.showing_on_its_workspace() || this._isDesktopWindow(window))
            return false;

        if (!this._windowIsOnThisMonitor(window))
            return false;

        const isSticky =
            window.is_on_all_workspaces() || window === this._movingWindow;

        // No workspace means we should show windows that are on all workspaces
        if (!this._workspace)
            return isSticky;

        // Otherwise only show windows that are (only) on that workspace
        return !isSticky && window.located_on_workspace(this._workspace);
    }

    _syncStacking() {
        const windowActors = global.get_window_actors().filter(w =>
            this._shouldShowWindow(w.meta_window));

        let lastRecord;
        const bottomActor = this._background ?? null;

        for (const windowActor of windowActors) {
            const record = this._windowRecords.find(r => r.windowActor === windowActor);

            this.set_child_above_sibling(record.clone,
                lastRecord ? lastRecord.clone : bottomActor);
            lastRecord = record;
        }
    }

    _isDesktopWindow(metaWindow) {
        return metaWindow.get_window_type() === Meta.WindowType.DESKTOP;
    }

    _windowIsOnThisMonitor(metawindow) {
        const geometry = global.display.get_monitor_geometry(this._monitor.index);
        const [intersects] = metawindow.get_frame_rect().intersect(geometry);
        return intersects;
    }

    _createDesktopWindows() {
        const desktopActors = global.get_window_actors().filter(w => {
            return this._isDesktopWindow(w.meta_window) && this._windowIsOnThisMonitor(w.meta_window);
        });
        desktopActors.map(a => this._createClone(a)).forEach(clone => this._background.add_child(clone));
    }

    _createWindows() {
        const windowActors = global.get_window_actors().filter(w =>
            this._shouldShowWindow(w.meta_window));

        windowActors.map(a => this._createClone(a)).forEach(clone => this.add_child(clone));
    }

    _createClone(windowActor) {
        const clone = new Clutter.Clone({
            source: windowActor,
            x: windowActor.x - this._monitor.x,
            y: windowActor.y - this._monitor.y,
        });

        const record = {windowActor, clone};

        windowActor.connectObject('destroy', () => {
            clone.destroy();
            this._windowRecords.splice(this._windowRecords.indexOf(record), 1);
        }, this);

        this._windowRecords.push(record);
        return clone;
    }

    _removeWindows() {
        for (const record of this._windowRecords)
            record.clone.destroy();

        this._windowRecords = [];
    }

    _onDestroy() {
        this._removeWindows();

        if (this._workspace)
            this._bgManager.destroy();
    }
});

export const MonitorGroup = GObject.registerClass({
    Properties: {
        'progress': GObject.ParamSpec.double(
            'progress', 'progress', 'progress',
            GObject.ParamFlags.READWRITE,
            -Infinity, Infinity, 0),
    },
}, class MonitorGroup extends St.Widget {
    _init(monitor, workspaceIndices, movingWindow) {
        super._init({
            clip_to_allocation: true,
            style_class: 'workspace-animation',
        });

        this._monitor = monitor;

        const constraint = new Layout.MonitorConstraint({index: monitor.index});
        this.add_constraint(constraint);

        this._container = new Clutter.Actor();
        this.add_child(this._container);

        const stickyGroup = new WorkspaceGroup(null, monitor, movingWindow);
        this.add_child(stickyGroup);

        this._workspaceGroups = [];

        const workspaceManager = global.workspace_manager;
        const vertical = workspaceManager.layout_rows === -1;
        const activeWorkspace = workspaceManager.get_active_workspace();

        let x = 0;
        let y = 0;

        for (const i of workspaceIndices) {
            const ws = workspaceManager.get_workspace_by_index(i);
            const fullscreen = ws.list_windows().some(w => w.get_monitor() === monitor.index && w.is_fullscreen());

            if (i > 0 && vertical && !fullscreen && monitor.index === Main.layoutManager.primaryIndex) {
                // We have to shift windows up or down by the height of the panel to prevent having a
                // visible gap between the windows while switching workspaces. Since fullscreen windows
                // hide the panel, they don't need to be shifted up or down.
                y -= Main.panel.height;
            }

            const group = new WorkspaceGroup(ws, monitor, movingWindow);

            this._workspaceGroups.push(group);
            this._container.add_child(group);
            group.set_position(x, y);

            if (vertical)
                y += this.baseDistance;
            else if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
                x -= this.baseDistance;
            else
                x += this.baseDistance;
        }

        this.progress = this.getWorkspaceProgress(activeWorkspace);

        if (monitor.index === Main.layoutManager.primaryIndex) {
            this._workspacesAdjustment = Main.createWorkspacesAdjustment(this);
            this.bind_property_full('progress',
                this._workspacesAdjustment, 'value',
                GObject.BindingFlags.SYNC_CREATE,
                (bind, source) => {
                    const indices = [
                        workspaceIndices[Math.floor(source)],
                        workspaceIndices[Math.ceil(source)],
                    ];
                    return [true, Util.lerp(...indices, source % 1.0)];
                },
                null);

            this.connect('destroy', () => {
                delete this._workspacesAdjustment;
            });
        }
    }

    get baseDistance() {
        const spacing = WORKSPACE_SPACING * St.ThemeContext.get_for_stage(global.stage).scale_factor;

        if (global.workspace_manager.layout_rows === -1)
            return this._monitor.height + spacing;
        else
            return this._monitor.width + spacing;
    }

    get progress() {
        if (global.workspace_manager.layout_rows === -1)
            return -this._container.y / this.baseDistance;
        else if (this.get_text_direction() === Clutter.TextDirection.RTL)
            return this._container.x / this.baseDistance;
        else
            return -this._container.x / this.baseDistance;
    }

    set progress(p) {
        if (global.workspace_manager.layout_rows === -1)
            this._container.y = -Math.round(p * this.baseDistance);
        else if (this.get_text_direction() === Clutter.TextDirection.RTL)
            this._container.x = Math.round(p * this.baseDistance);
        else
            this._container.x = -Math.round(p * this.baseDistance);

        this.notify('progress');
    }

    get index() {
        return this._monitor.index;
    }

    getWorkspaceProgress(workspace) {
        const group = this._workspaceGroups.find(g =>
            g.workspace.index() === workspace.index());
        return this._getWorkspaceGroupProgress(group);
    }

    _getWorkspaceGroupProgress(group) {
        if (global.workspace_manager.layout_rows === -1)
            return group.y / this.baseDistance;
        else if (this.get_text_direction() === Clutter.TextDirection.RTL)
            return -group.x / this.baseDistance;
        else
            return group.x / this.baseDistance;
    }

    getSnapPoints() {
        return this._workspaceGroups.map(g =>
            this._getWorkspaceGroupProgress(g));
    }

    findClosestWorkspace(progress) {
        const distances = this.getSnapPoints().map(p =>
            Math.abs(p - progress));
        const index = distances.indexOf(Math.min(...distances));
        return this._workspaceGroups[index].workspace;
    }

    _interpolateProgress(progress, monitorGroup) {
        if (this.index === monitorGroup.index)
            return progress;

        const points1 = monitorGroup.getSnapPoints();
        const points2 = this.getSnapPoints();

        const upper = points1.indexOf(points1.find(p => p >= progress));
        const lower = points1.indexOf(points1.slice().reverse().find(p => p <= progress));

        if (points1[upper] === points1[lower])
            return points2[upper];

        const t = (progress - points1[lower]) / (points1[upper] - points1[lower]);

        return points2[lower] + (points2[upper] - points2[lower]) * t;
    }

    updateSwipeForMonitor(progress, monitorGroup) {
        this.progress = this._interpolateProgress(progress, monitorGroup);
    }
});

export class WorkspaceAnimationController {
    constructor() {
        this._movingWindow = null;
        this._switchData = null;

        Main.overview.connect('showing', () => {
            if (this._switchData) {
                if (this._switchData.gestureActivated)
                    this._finishWorkspaceSwitch(this._switchData);
                this._swipeTracker.enabled = false;
            }
        });
        Main.overview.connect('hiding', () => {
            this._swipeTracker.enabled = true;
        });

        const swipeTracker = new SwipeTracker.SwipeTracker(global.stage,
            Clutter.Orientation.HORIZONTAL,
            Shell.ActionMode.NORMAL,
            {allowDrag: false});
        swipeTracker.connect('begin', this._switchWorkspaceBegin.bind(this));
        swipeTracker.connect('update', this._switchWorkspaceUpdate.bind(this));
        swipeTracker.connect('end', this._switchWorkspaceEnd.bind(this));
        this._swipeTracker = swipeTracker;

        global.display.bind_property('compositor-modifiers',
            this._swipeTracker, 'scroll-modifiers',
            GObject.BindingFlags.SYNC_CREATE);
    }

    _prepareWorkspaceSwitch(workspaceIndices) {
        if (this._switchData)
            return;

        const workspaceManager = global.workspace_manager;
        const nWorkspaces = workspaceManager.get_n_workspaces();

        const switchData = {};

        this._switchData = switchData;
        switchData.monitors = [];

        switchData.gestureActivated = false;
        switchData.inProgress = false;

        if (!workspaceIndices)
            workspaceIndices = [...Array(nWorkspaces).keys()];

        const monitors = Meta.prefs_get_workspaces_only_on_primary()
            ? [Main.layoutManager.primaryMonitor] : Main.layoutManager.monitors;

        for (const monitor of monitors) {
            if (Meta.prefs_get_workspaces_only_on_primary() &&
                monitor.index !== Main.layoutManager.primaryIndex)
                continue;

            const group = new MonitorGroup(monitor, workspaceIndices, this.movingWindow);

            Main.uiGroup.insert_child_above(group, global.window_group);

            switchData.monitors.push(group);
        }

        Meta.disable_unredirect_for_display(global.display);
    }

    _finishWorkspaceSwitch(switchData) {
        Meta.enable_unredirect_for_display(global.display);

        this._switchData = null;

        switchData.monitors.forEach(m => m.destroy());

        this.movingWindow = null;
    }

    animateSwitch(from, to, direction, onComplete) {
        this._swipeTracker.enabled = false;

        let workspaceIndices = [];

        switch (direction) {
        case Meta.MotionDirection.UP:
        case Meta.MotionDirection.LEFT:
        case Meta.MotionDirection.UP_LEFT:
        case Meta.MotionDirection.UP_RIGHT:
            workspaceIndices = [to, from];
            break;

        case Meta.MotionDirection.DOWN:
        case Meta.MotionDirection.RIGHT:
        case Meta.MotionDirection.DOWN_LEFT:
        case Meta.MotionDirection.DOWN_RIGHT:
            workspaceIndices = [from, to];
            break;
        }

        if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL &&
            direction !== Meta.MotionDirection.UP &&
            direction !== Meta.MotionDirection.DOWN)
            workspaceIndices.reverse();

        this._prepareWorkspaceSwitch(workspaceIndices);
        this._switchData.inProgress = true;

        const fromWs = global.workspace_manager.get_workspace_by_index(from);
        const toWs = global.workspace_manager.get_workspace_by_index(to);

        for (const monitorGroup of this._switchData.monitors) {
            monitorGroup.progress = monitorGroup.getWorkspaceProgress(fromWs);
            const progress = monitorGroup.getWorkspaceProgress(toWs);

            const params = {
                duration: WINDOW_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            };

            if (monitorGroup.index === Main.layoutManager.primaryIndex) {
                params.onComplete = () => {
                    this._finishWorkspaceSwitch(this._switchData);
                    onComplete();
                    this._swipeTracker.enabled = true;
                };
            }

            monitorGroup.ease_property('progress', progress, params);
        }
    }

    canHandleScrollEvent(event) {
        return this._swipeTracker.canHandleScrollEvent(event);
    }

    _findMonitorGroup(monitorIndex) {
        return this._switchData.monitors.find(m => m.index === monitorIndex);
    }

    _switchWorkspaceBegin(tracker, monitor) {
        if (Meta.prefs_get_workspaces_only_on_primary() &&
            monitor !== Main.layoutManager.primaryIndex)
            return;

        const workspaceManager = global.workspace_manager;
        const horiz = workspaceManager.layout_rows !== -1;
        tracker.orientation = horiz
            ? Clutter.Orientation.HORIZONTAL
            : Clutter.Orientation.VERTICAL;

        if (this._switchData && this._switchData.gestureActivated) {
            for (const group of this._switchData.monitors)
                group.remove_all_transitions();
        } else {
            this._prepareWorkspaceSwitch();
        }

        const monitorGroup = this._findMonitorGroup(monitor);
        const baseDistance = monitorGroup.baseDistance;
        const progress = monitorGroup.progress;

        const closestWs = monitorGroup.findClosestWorkspace(progress);
        const cancelProgress = monitorGroup.getWorkspaceProgress(closestWs);
        const points = monitorGroup.getSnapPoints();

        this._switchData.baseMonitorGroup = monitorGroup;

        tracker.confirmSwipe(baseDistance, points, progress, cancelProgress);
    }

    _switchWorkspaceUpdate(tracker, progress) {
        if (!this._switchData)
            return;

        for (const monitorGroup of this._switchData.monitors)
            monitorGroup.updateSwipeForMonitor(progress, this._switchData.baseMonitorGroup);
    }

    _switchWorkspaceEnd(tracker, duration, endProgress) {
        if (!this._switchData)
            return;

        const switchData = this._switchData;
        switchData.gestureActivated = true;

        const newWs = switchData.baseMonitorGroup.findClosestWorkspace(endProgress);
        const endTime = Clutter.get_current_event_time();

        for (const monitorGroup of this._switchData.monitors) {
            const progress = monitorGroup.getWorkspaceProgress(newWs);

            const params = {
                duration,
                mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            };

            if (monitorGroup.index === Main.layoutManager.primaryIndex) {
                params.onComplete = () => {
                    if (!newWs.active)
                        newWs.activate(endTime);
                    this._finishWorkspaceSwitch(switchData);
                };
            }

            monitorGroup.ease_property('progress', progress, params);
        }
    }

    get gestureActive() {
        return this._switchData !== null && this._switchData.gestureActivated;
    }

    cancelSwitchAnimation() {
        if (!this._switchData)
            return;

        if (this._switchData.gestureActivated)
            return;

        this._finishWorkspaceSwitch(this._switchData);
    }

    set movingWindow(movingWindow) {
        this._movingWindow = movingWindow;
    }

    get movingWindow() {
        return this._movingWindow;
    }
}
(uuay)appFavorites.jsG// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Shell from 'gi://Shell';
import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
import * as Signals from '../misc/signals.js';

import * as Main from './main.js';

// In alphabetical order
const RENAMED_DESKTOP_IDS = {
    'baobab.desktop': 'org.gnome.baobab.desktop',
    'cheese.desktop': 'org.gnome.Cheese.desktop',
    'dconf-editor.desktop': 'ca.desrt.dconf-editor.desktop',
    'empathy.desktop': 'org.gnome.Empathy.desktop',
    'eog.desktop': 'org.gnome.eog.desktop',
    'epiphany.desktop': 'org.gnome.Epiphany.desktop',
    'evolution.desktop': 'org.gnome.Evolution.desktop',
    'file-roller.desktop': 'org.gnome.FileRoller.desktop',
    'five-or-more.desktop': 'org.gnome.five-or-more.desktop',
    'four-in-a-row.desktop': 'org.gnome.Four-in-a-row.desktop',
    'gcalctool.desktop': 'org.gnome.Calculator.desktop',
    'geary.desktop': 'org.gnome.Geary.desktop',
    'gedit.desktop': 'org.gnome.gedit.desktop',
    'glchess.desktop': 'org.gnome.Chess.desktop',
    'glines.desktop': 'org.gnome.five-or-more.desktop',
    'gnect.desktop': 'org.gnome.Four-in-a-row.desktop',
    'gnibbles.desktop': 'org.gnome.Nibbles.desktop',
    'gnobots2.desktop': 'org.gnome.Robots.desktop',
    'gnome-boxes.desktop': 'org.gnome.Boxes.desktop',
    'gnome-calculator.desktop': 'org.gnome.Calculator.desktop',
    'gnome-chess.desktop': 'org.gnome.Chess.desktop',
    'gnome-clocks.desktop': 'org.gnome.clocks.desktop',
    'gnome-contacts.desktop': 'org.gnome.Contacts.desktop',
    'gnome-documents.desktop': 'org.gnome.Documents.desktop',
    'gnome-font-viewer.desktop': 'org.gnome.font-viewer.desktop',
    'gnome-klotski.desktop': 'org.gnome.Klotski.desktop',
    'gnome-nibbles.desktop': 'org.gnome.Nibbles.desktop',
    'gnome-mahjongg.desktop': 'org.gnome.Mahjongg.desktop',
    'gnome-mines.desktop': 'org.gnome.Mines.desktop',
    'gnome-music.desktop': 'org.gnome.Music.desktop',
    'gnome-photos.desktop': 'org.gnome.Photos.desktop',
    'gnome-robots.desktop': 'org.gnome.Robots.desktop',
    'gnome-screenshot.desktop': 'org.gnome.Screenshot.desktop',
    'gnome-software.desktop': 'org.gnome.Software.desktop',
    'gnome-terminal.desktop': 'org.gnome.Terminal.desktop',
    'gnome-tetravex.desktop': 'org.gnome.Tetravex.desktop',
    'gnome-tweaks.desktop': 'org.gnome.tweaks.desktop',
    'gnome-weather.desktop': 'org.gnome.Weather.desktop',
    'gnomine.desktop': 'org.gnome.Mines.desktop',
    'gnotravex.desktop': 'org.gnome.Tetravex.desktop',
    'gnotski.desktop': 'org.gnome.Klotski.desktop',
    'gtali.desktop': 'org.gnome.Tali.desktop',
    'iagno.desktop': 'org.gnome.Reversi.desktop',
    'nautilus.desktop': 'org.gnome.Nautilus.desktop',
    'org.gnome.gnome-2048.desktop': 'org.gnome.TwentyFortyEight.desktop',
    'org.gnome.taquin.desktop': 'org.gnome.Taquin.desktop',
    'org.gnome.Weather.Application.desktop': 'org.gnome.Weather.desktop',
    'polari.desktop': 'org.gnome.Polari.desktop',
    'seahorse.desktop': 'org.gnome.seahorse.Application.desktop',
    'shotwell.desktop': 'org.gnome.Shotwell.desktop',
    'simple-scan.desktop': 'org.gnome.SimpleScan.desktop',
    'tali.desktop': 'org.gnome.Tali.desktop',
    'totem.desktop': 'org.gnome.Totem.desktop',
    'evince.desktop': 'org.gnome.Evince.desktop',
};

class AppFavorites extends Signals.EventEmitter {
    constructor() {
        super();

        // Filter the apps through the user’s parental controls.
        this._parentalControlsManager = ParentalControlsManager.getDefault();
        this._parentalControlsManager.connect('app-filter-changed', () => {
            this.reload();
            this.emit('changed');
        });

        this.FAVORITE_APPS_KEY = 'favorite-apps';
        this._favorites = {};
        global.settings.connect(`changed::${this.FAVORITE_APPS_KEY}`, this._onFavsChanged.bind(this));
        this.reload();
    }

    _onFavsChanged() {
        this.reload();
        this.emit('changed');
    }

    reload() {
        let ids = global.settings.get_strv(this.FAVORITE_APPS_KEY);
        let appSys = Shell.AppSystem.get_default();

        // Map old desktop file names to the current ones
        let updated = false;
        ids = ids.map(id => {
            let newId = RENAMED_DESKTOP_IDS[id];
            if (newId !== undefined &&
                appSys.lookup_app(newId) != null) {
                updated = true;
                return newId;
            }
            return id;
        });
        // ... and write back the updated desktop file names
        if (updated)
            global.settings.set_strv(this.FAVORITE_APPS_KEY, ids);

        let apps = ids.map(id => appSys.lookup_app(id))
                      .filter(app => app !== null && this._parentalControlsManager.shouldShowApp(app.app_info));
        this._favorites = {};
        for (let i = 0; i < apps.length; i++) {
            let app = apps[i];
            this._favorites[app.get_id()] = app;
        }
    }

    _getIds() {
        let ret = [];
        for (let id in this._favorites)
            ret.push(id);
        return ret;
    }

    getFavoriteMap() {
        return this._favorites;
    }

    getFavorites() {
        let ret = [];
        for (let id in this._favorites)
            ret.push(this._favorites[id]);
        return ret;
    }

    isFavorite(appId) {
        return appId in this._favorites;
    }

    _addFavorite(appId, pos) {
        if (appId in this._favorites)
            return false;

        let app = Shell.AppSystem.get_default().lookup_app(appId);

        if (!app)
            return false;

        if (!this._parentalControlsManager.shouldShowApp(app.app_info))
            return false;

        let ids = this._getIds();
        if (pos === -1)
            ids.push(appId);
        else
            ids.splice(pos, 0, appId);
        global.settings.set_strv(this.FAVORITE_APPS_KEY, ids);
        return true;
    }

    addFavoriteAtPos(appId, pos) {
        if (!this._addFavorite(appId, pos))
            return;

        let app = Shell.AppSystem.get_default().lookup_app(appId);

        let msg = _('%s has been pinned to the dash.').format(app.get_name());
        Main.overview.setMessage(msg, {
            forFeedback: true,
            undoCallback: () => this._removeFavorite(appId),
        });
    }

    addFavorite(appId) {
        this.addFavoriteAtPos(appId, -1);
    }

    moveFavoriteToPos(appId, pos) {
        this._removeFavorite(appId);
        this._addFavorite(appId, pos);
    }

    _removeFavorite(appId) {
        if (!(appId in this._favorites))
            return false;

        let ids = this._getIds().filter(id => id !== appId);
        global.settings.set_strv(this.FAVORITE_APPS_KEY, ids);
        return true;
    }

    removeFavorite(appId) {
        let ids = this._getIds();
        let pos = ids.indexOf(appId);

        let app = this._favorites[appId];
        if (!this._removeFavorite(appId))
            return;

        let msg = _('%s has been unpinned from the dash.').format(app.get_name());
        Main.overview.setMessage(msg, {
            forFeedback: true,
            undoCallback: () => this._addFavorite(appId, pos),
        });
    }
}

var appFavoritesInstance = null;

/**
 * @returns {AppFavorites}
 */
export function getAppFavorites() {
    if (appFavoritesInstance == null)
        appFavoritesInstance = new AppFavorites();
    return appFavoritesInstance;
}
(uuay)main.jsݐ// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as AccessDialog from './accessDialog.js';
import * as AudioDeviceSelection from './audioDeviceSelection.js';
import * as Config from '../misc/config.js';
import * as Components from './components.js';
import * as CtrlAltTab from './ctrlAltTab.js';
import * as EndSessionDialog from './endSessionDialog.js';
import * as ExtensionSystem from './extensionSystem.js';
import * as ExtensionDownloader from './extensionDownloader.js';
import * as InputMethod from '../misc/inputMethod.js';
import * as Introspect from '../misc/introspect.js';
import * as Keyboard from './keyboard.js';
import * as MessageTray from './messageTray.js';
import * as ModalDialog from './modalDialog.js';
import * as OsdWindow from './osdWindow.js';
import * as OsdMonitorLabeler from './osdMonitorLabeler.js';
import * as Overview from './overview.js';
import * as PadOsd from './padOsd.js';
import * as Panel from './panel.js';
import * as RunDialog from './runDialog.js';
import * as WelcomeDialog from './welcomeDialog.js';
import * as Layout from './layout.js';
import * as LoginManager from '../misc/loginManager.js';
import * as LookingGlass from './lookingGlass.js';
import * as NotificationDaemon from './notificationDaemon.js';
import * as WindowAttentionHandler from './windowAttentionHandler.js';
import * as Screenshot from './screenshot.js';
import * as ScreenShield from './screenShield.js';
import * as SessionMode from './sessionMode.js';
import * as ShellDBus from './shellDBus.js';
import * as ShellMountOperation from './shellMountOperation.js';
import * as WindowManager from './windowManager.js';
import * as Magnifier from './magnifier.js';
import * as XdndHandler from './xdndHandler.js';
import * as KbdA11yDialog from './kbdA11yDialog.js';
import * as LocatePointer from './locatePointer.js';
import * as PointerA11yTimeout from './pointerA11yTimeout.js';
import {formatError} from '../misc/errorUtils.js';
import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
import * as Util from '../misc/util.js';

const WELCOME_DIALOG_LAST_SHOWN_VERSION = 'welcome-dialog-last-shown-version';
// Make sure to mention the point release, otherwise it will show every time
// until this version is current
const WELCOME_DIALOG_LAST_TOUR_CHANGE = '40.beta';
const LOG_DOMAIN = 'GNOME Shell';
const GNOMESHELL_STARTED_MESSAGE_ID = 'f3ea493c22934e26811cd62abe8e203a';

export let componentManager = null;
export let extensionManager = null;
export let panel = null;
export let overview = null;
export let runDialog = null;
export let lookingGlass = null;
export let welcomeDialog = null;
export let wm = null;
export let messageTray = null;
export let screenShield = null;
export let notificationDaemon = null;
export let windowAttentionHandler = null;
export let ctrlAltTabManager = null;
export let padOsdService = null;
export let osdWindowManager = null;
export let osdMonitorLabeler = null;
export let sessionMode = null;
export let screenshotUI = null;
export let shellAccessDialogDBusService = null;
export let shellAudioSelectionDBusService = null;
export let shellDBusService = null;
export let shellMountOpDBusService = null;
export let screenSaverDBus = null;
export let modalCount = 0;
export let actionMode = Shell.ActionMode.NONE;
export let modalActorFocusStack = [];
export let uiGroup = null;
export let magnifier = null;
export let xdndHandler = null;
export let keyboard = null;
export let layoutManager = null;
export let kbdA11yDialog = null;
export let inputMethod = null;
export let introspectService = null;
export let locatePointer = null;
export let endSessionDialog = null;

let _startDate;
let _defaultCssStylesheet = null;
let _cssStylesheet = null;
let _themeResource = null;
let _oskResource = null;
let _iconResource = null;
let _workspacesAdjustment = null;
let _workspaceAdjustmentRegistry = null;

Gio._promisify(Gio.File.prototype, 'delete_async');
Gio._promisify(Gio.File.prototype, 'touch_async');

let _remoteAccessInhibited = false;

function _sessionUpdated() {
    if (sessionMode.isPrimary)
        _loadDefaultStylesheet();

    wm.allowKeybinding('overlay-key',
        Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW);

    wm.allowKeybinding('locate-pointer-key', Shell.ActionMode.ALL);

    wm.setCustomKeybindingHandler('panel-run-dialog',
        Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
        sessionMode.hasRunDialog ? openRunDialog : null);

    if (!sessionMode.hasRunDialog) {
        if (runDialog)
            runDialog.close();
        if (lookingGlass)
            lookingGlass.close();
        if (welcomeDialog)
            welcomeDialog.close();
    }

    let remoteAccessController = global.backend.get_remote_access_controller();
    if (remoteAccessController && !global.backend.is_headless()) {
        if (sessionMode.allowScreencast && _remoteAccessInhibited) {
            remoteAccessController.uninhibit_remote_access();
            _remoteAccessInhibited = false;
        } else if (!sessionMode.allowScreencast && !_remoteAccessInhibited) {
            remoteAccessController.inhibit_remote_access();
            _remoteAccessInhibited = true;
        }
    }

    if (!GLib.getenv('SHELL_DEBUG')) {
        if (typeof sessionMode.debugFlags === 'string')
            global.set_debug_flags(sessionMode.debugFlags);
        else if (Array.isArray(sessionMode.debugFlags))
            global.set_debug_flags(sessionMode.debugFlags.join(':'))
    }
}

/** @returns {void} */
export async function start() {
    globalThis.log = console.log;
    globalThis.logError = function (err, msg) {
        const args = [formatError(err)];
        try {
            // toString() can throw
            if (msg)
                args.unshift(`${msg}:`);
        } catch (e) {}

        console.error(...args);
    };

    // Chain up async errors reported from C
    global.connect('notify-error', (global, msg, detail) => {
        notifyError(msg, detail);
    });

    let currentDesktop = GLib.getenv('XDG_CURRENT_DESKTOP');
    if (!currentDesktop || !currentDesktop.split(':').includes('GNOME'))
        Gio.DesktopAppInfo.set_desktop_env('GNOME');

    sessionMode = new SessionMode.SessionMode();
    sessionMode.connect('updated', _sessionUpdated);

    St.Settings.get().connect('notify::high-contrast', _loadDefaultStylesheet);
    St.Settings.get().connect('notify::color-scheme', _loadDefaultStylesheet);
    St.Settings.get().connect('notify::gtk-theme', _loadDefaultStylesheet);
    St.Settings.get().connect('notify::gtk-theme-variant', _loadDefaultStylesheet);
    St.Settings.get().connect('notify::shell-color-scheme', _loadDefaultStylesheet);

    // Initialize ParentalControlsManager before the UI
    ParentalControlsManager.getDefault();

    await _initializeUI();

    shellAccessDialogDBusService = new AccessDialog.AccessDialogDBus();
    shellAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus();
    shellDBusService = new ShellDBus.GnomeShell();
    shellMountOpDBusService = new ShellMountOperation.GnomeShellMountOpHandler();

    const watchId = Gio.DBus.session.watch_name('org.gnome.Shell.Notifications',
        Gio.BusNameWatcherFlags.AUTO_START,
        bus => bus.unwatch_name(watchId),
        bus => bus.unwatch_name(watchId));

    _sessionUpdated();
}

/** @private */
async function _initializeUI() {
    // Ensure ShellWindowTracker and ShellAppUsage are initialized; this will
    // also initialize ShellAppSystem first. ShellAppSystem
    // needs to load all the .desktop files, and ShellWindowTracker
    // will use those to associate with windows. Right now
    // the Monitor doesn't listen for installed app changes
    // and recalculate application associations, so to avoid
    // races for now we initialize it here. It's better to
    // be predictable anyways.
    Shell.WindowTracker.get_default();
    Shell.AppUsage.get_default();

    reloadThemeResource();
    _loadIcons();
    _loadOskLayouts();
    _loadDefaultStylesheet();
    _loadWorkspacesAdjustment();

    new AnimationsSettings();

    // Setup the stage hierarchy early
    layoutManager = new Layout.LayoutManager();

    // Various parts of the codebase still refer to Main.uiGroup
    // instead of using the layoutManager. This keeps that code
    // working until it's updated.
    uiGroup = layoutManager.uiGroup;

    padOsdService = new PadOsd.PadOsdService();
    xdndHandler = new XdndHandler.XdndHandler();
    ctrlAltTabManager = new CtrlAltTab.CtrlAltTabManager();
    osdWindowManager = new OsdWindow.OsdWindowManager();
    osdMonitorLabeler = new OsdMonitorLabeler.OsdMonitorLabeler();
    overview = new Overview.Overview();
    kbdA11yDialog = new KbdA11yDialog.KbdA11yDialog();
    wm = new WindowManager.WindowManager();
    magnifier = new Magnifier.Magnifier();
    locatePointer = new LocatePointer.LocatePointer();

    if (LoginManager.canLock())
        screenShield = new ScreenShield.ScreenShield();

    inputMethod = new InputMethod.InputMethod();
    Clutter.get_default_backend().set_input_method(inputMethod);
    global.connect('shutdown',
        () => Clutter.get_default_backend().set_input_method(null));

    screenshotUI = new Screenshot.ScreenshotUI();

    messageTray = new MessageTray.MessageTray();
    panel = new Panel.Panel();
    keyboard = new Keyboard.KeyboardManager();
    notificationDaemon = new NotificationDaemon.NotificationDaemon();
    windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
    componentManager = new Components.ComponentManager();

    introspectService = new Introspect.IntrospectService();

    layoutManager.init();
    overview.init();

    new PointerA11yTimeout.PointerA11yTimeout();

    global.connect('locate-pointer', () => {
        locatePointer.show();
    });

    global.display.connect('show-restart-message', (display, message) => {
        showRestartMessage(message);
        return true;
    });

    global.display.connect('restart', () => {
        global.reexec_self();
        return true;
    });

    global.display.connect('gl-video-memory-purged', loadTheme);

    global.context.connect('notify::unsafe-mode', () => {
        if (!global.context.unsafe_mode)
            return; // we're safe
        if (lookingGlass?.isOpen)
            return; // assume user action

        const source = MessageTray.getSystemSource();
        const notification = new MessageTray.Notification({
            source,
            title: _('System was put in unsafe mode'),
            body: _('Apps now have unrestricted access'),
            isTransient: true,
        });
        notification.addAction(_('Undo'),
            () => (global.context.unsafe_mode = false));
        source.addNotification(notification);
    });

    // Provide the bus object for gnome-session to
    // initiate logouts.
    endSessionDialog = new EndSessionDialog.EndSessionDialog();

    // We're ready for the session manager to move to the next phase
    GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
        Shell.util_sd_notify();
        global.context.notify_ready();
        return GLib.SOURCE_REMOVE;
    });

    _startDate = new Date();

    ExtensionDownloader.init();
    extensionManager = new ExtensionSystem.ExtensionManager();
    extensionManager.init();

    if (sessionMode.isGreeter && screenShield) {
        layoutManager.connect('startup-prepared', () => {
            screenShield.showDialog();
        });
    }

    let Scripting;
    let perfModule;
    const {automationScript} = global;
    if (automationScript) {
        Scripting = await import('./scripting.js');
        perfModule = await import(automationScript.get_uri());
        if (perfModule.init)
            perfModule.init();
    }

    layoutManager.connect('startup-complete', () => {
        if (actionMode === Shell.ActionMode.NONE)
            actionMode = Shell.ActionMode.NORMAL;

        if (screenShield)
            screenShield.lockIfWasLocked();

        if (sessionMode.currentMode !== 'gdm' &&
            sessionMode.currentMode !== 'initial-setup') {
            GLib.log_structured(LOG_DOMAIN, GLib.LogLevelFlags.LEVEL_MESSAGE, {
                'MESSAGE': `GNOME Shell started at ${_startDate}`,
                'MESSAGE_ID': GNOMESHELL_STARTED_MESSAGE_ID,
            });
        }

        if (!perfModule) {
            let credentials = new Gio.Credentials();
            if (credentials.get_unix_user() === 0) {
                notify(
                    _('Logged in as a privileged user'),
                    _('Running a session as a privileged user should be avoided for security reasons. If possible, you should log in as a normal user.'));
            } else if (sessionMode.showWelcomeDialog) {
                _handleShowWelcomeScreen();
            }
        }

        if (sessionMode.currentMode !== 'gdm' &&
            sessionMode.currentMode !== 'initial-setup')
            _handleLockScreenWarning();

        LoginManager.registerSessionWithGDM();

        if (perfModule) {
            let perfOutput = GLib.getenv('SHELL_PERF_OUTPUT');
            Scripting.runPerfScript(perfModule, perfOutput);
        }
    });
}

function _handleShowWelcomeScreen() {
    const lastShownVersion = global.settings.get_string(WELCOME_DIALOG_LAST_SHOWN_VERSION);
    if (Util.GNOMEversionCompare(WELCOME_DIALOG_LAST_TOUR_CHANGE, lastShownVersion) > 0) {
        openWelcomeDialog();
        global.settings.set_string(WELCOME_DIALOG_LAST_SHOWN_VERSION, Config.PACKAGE_VERSION);
    }
}

async function _handleLockScreenWarning() {
    const path = `${global.userdatadir}/lock-warning-shown`;
    const file = Gio.File.new_for_path(path);

    const hasLockScreen = screenShield !== null;
    if (hasLockScreen) {
        try {
            await file.delete_async(0, null);
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
                logError(e);
        }
    } else {
        try {
            if (!await file.touch_async())
                return;
        } catch (e) {
            logError(e);
        }

        notify(
            _('Screen Lock disabled'),
            _('Screen Locking requires the GNOME display manager.'));
    }
}

function _realpath(path) {
    try {
        while (GLib.file_test(path, GLib.FileTest.IS_SYMLINK))
            path = GLib.file_read_link(path);
    } catch (e) { }
    return path;
}

function _getStylesheet(name) {
    let stylesheet;

    stylesheet = Gio.File.new_for_uri(`resource:///org/gnome/shell/theme/${name}`);
    if (stylesheet.query_exists(null))
        return stylesheet;

    let dataDirs = GLib.get_system_data_dirs();
    for (let i = 0; i < dataDirs.length; i++) {
        let path = GLib.build_filenamev([dataDirs[i], 'gnome-shell', 'theme', name]);
        stylesheet = Gio.file_new_for_path(_realpath(path));
        if (stylesheet.query_exists(null))
            return stylesheet;
    }

    stylesheet = Gio.File.new_for_path(_realpath(`${global.datadir}/theme/${name}`));
    if (stylesheet.query_exists(null))
        return stylesheet;

    return null;
}

/** @returns {string} */
export function getStyleVariant() {
    const {colorScheme} = St.Settings.get();
    switch (sessionMode.colorScheme) {
    case 'force-dark':
        return 'dark';
    case 'force-light':
        return 'light';
    case 'prefer-dark':
        return colorScheme === St.SystemColorScheme.PREFER_LIGHT
            ? 'light' : 'dark';
    case 'prefer-light':
        return colorScheme === St.SystemColorScheme.PREFER_DARK
            ? 'dark' : 'light';
    default:
        return '';
    }
}

function _getYaruStyleSheet(themeVariant) {
    const styleVariant = getStyleVariant() || 'light';
    const baseThemeName = sessionMode.stylesheetName.split(".css").at(0);
    const isDark = themeVariant === 'dark' || themeVariant?.endsWith('-dark');
    let colorSchemeVariant;

    if (isDark && styleVariant === 'light') {
        colorSchemeVariant = themeVariant.split('-').slice(0, -1).join('-');
    } else if (!isDark && styleVariant == 'dark' ) {
        colorSchemeVariant = themeVariant ? `${themeVariant}-dark` : 'dark';
    }

    if (colorSchemeVariant !== undefined) {
        if (colorSchemeVariant.length)
            colorSchemeVariant = `-${colorSchemeVariant}`;
        const stylesheet = _getStylesheet(`${baseThemeName}${colorSchemeVariant}.css`);
        if (stylesheet)
            return stylesheet;
    }

    if (!themeVariant)
        return null;

    const stylesheet = _getStylesheet(`${baseThemeName}-${themeVariant}.css`);

    // Try to use the dark theme if a dark variant is selected
    if (!stylesheet && isDark)
        return _getStylesheet(`${baseThemeName}-dark.css`);

    return stylesheet;
}

function _getDefaultStylesheet() {
    let stylesheet = null;
    let name = sessionMode.stylesheetName;

    // Look for a high-contrast variant first
    if (St.Settings.get().high_contrast)
        stylesheet = _getStylesheet(name.replace('.css', '-high-contrast.css'));

    if (stylesheet == null) {
        const settings = St.Settings.get();

        if (settings.gtkTheme === 'Yaru')
            stylesheet = _getYaruStyleSheet(settings.gtkThemeVariant?.toLowerCase());
    }

    if (stylesheet === null)
        stylesheet = _getStylesheet(name.replace('.css', `-${getStyleVariant()}.css`));

    if (stylesheet == null)
        stylesheet = _getStylesheet(name);

    return stylesheet;
}

function _loadDefaultStylesheet() {
    let stylesheet = _getDefaultStylesheet();
    if (_defaultCssStylesheet && _defaultCssStylesheet.equal(stylesheet))
        return;

    _defaultCssStylesheet = stylesheet;
    loadTheme();
}

class AdjustmentRegistry {
    #count = 0;
    #adjustments = new Map();
    #registry = new FinalizationRegistry(key => {
        this.#adjustments.delete(key);
    });

    register(adj) {
        const key = this.#count++;
        this.#adjustments.set(key, new WeakRef(adj));
        this.#registry.register(adj, key);
    }

    forEach(callback) {
        this.#adjustments.forEach((ref, key) => {
            const adj = ref.deref();
            if (adj)
                callback(adj);
            else
                this.#adjustments.delete(key);
        });
    }
}

function _loadWorkspacesAdjustment() {
    const {workspaceManager} = global;
    const activeWorkspaceIndex = workspaceManager.get_active_workspace_index();

    _workspacesAdjustment = new St.Adjustment({
        value: activeWorkspaceIndex,
        lower: 0,
        page_increment: 1,
        page_size: 1,
        step_increment: 0,
        upper: workspaceManager.n_workspaces,
    });

    workspaceManager.bind_property('n-workspaces',
        _workspacesAdjustment, 'upper',
        GObject.BindingFlags.SYNC_CREATE);

    _workspacesAdjustment.connect('notify::upper', () => {
        const newActiveIndex = workspaceManager.get_active_workspace_index();

        // A workspace might have been inserted or removed before the active
        // one, causing the adjustment to go out of sync, so update the value
        _workspaceAdjustmentRegistry.forEach(c => c.remove_transition('value'));
        _workspacesAdjustment.remove_transition('value');
        _workspacesAdjustment.value = newActiveIndex;
    });

    _workspaceAdjustmentRegistry = new AdjustmentRegistry();
}

/**
 * Creates an adjustment that has its lower, upper, and value
 * properties set for the number of available workspaces. Consumers
 * of the returned adjustment must only change the 'value' property,
 * and only that.
 *
 * @param {Clutter.Actor} actor
 *
 * @returns {St.Adjustment} - an adjustment representing the
 * current workspaces layout
 */
export function createWorkspacesAdjustment(actor) {
    const adjustment = new St.Adjustment({actor});

    const properties = [
        ['lower', GObject.BindingFlags.SYNC_CREATE],
        ['page-increment', GObject.BindingFlags.SYNC_CREATE],
        ['page-size', GObject.BindingFlags.SYNC_CREATE],
        ['step-increment', GObject.BindingFlags.SYNC_CREATE],
        ['upper', GObject.BindingFlags.SYNC_CREATE],
        ['value', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL],
    ];

    for (const [propName, flags] of properties)
        _workspacesAdjustment.bind_property(propName, adjustment, propName, flags);

    _workspaceAdjustmentRegistry.register(adjustment);

    return adjustment;
}

/**
 * Get the theme CSS file that the shell will load
 *
 * @returns {?Gio.File}: A #GFile that contains the theme CSS,
 *          null if using the default
 */
export function getThemeStylesheet() {
    return _cssStylesheet;
}

/**
 * Set the theme CSS file that the shell will load
 *
 * @param {string=} cssStylesheet - A file path that contains the theme CSS,
 *     set it to null to use the default
 */
export function setThemeStylesheet(cssStylesheet) {
    _cssStylesheet = cssStylesheet ? Gio.File.new_for_path(cssStylesheet) : null;
}

export function reloadThemeResource() {
    if (_themeResource)
        _themeResource._unregister();

    _themeResource = Gio.Resource.load(
        `${global.datadir}/${sessionMode.themeResourceName}`);
    _themeResource._register();
}

/** @private */
function _loadIcons() {
    _iconResource = Gio.Resource.load(`${global.datadir}/${sessionMode.iconsResourceName}`);
    _iconResource._register();
}

function _loadOskLayouts() {
    _oskResource = Gio.Resource.load(`${global.datadir}/gnome-shell-osk-layouts.gresource`);
    _oskResource._register();
}

/**
 * loadTheme:
 *
 * Reloads the theme CSS file
 */
export function loadTheme() {
    let themeContext = St.ThemeContext.get_for_stage(global.stage);
    let previousTheme = themeContext.get_theme();

    let theme = new St.Theme({
        application_stylesheet: _cssStylesheet,
        default_stylesheet: _defaultCssStylesheet,
    });

    if (theme.default_stylesheet == null)
        throw new Error(`No valid stylesheet found for '${sessionMode.stylesheetName}'`);

    if (previousTheme) {
        let customStylesheets = previousTheme.get_custom_stylesheets();

        for (let i = 0; i < customStylesheets.length; i++)
            theme.load_stylesheet(customStylesheets[i]);
    }

    themeContext.set_theme(theme);
}

/**
 * @param {string} msg A message
 * @param {string} details Additional information
 */
export function notify(msg, details) {
    const source = MessageTray.getSystemSource();
    const notification = new MessageTray.Notification({
        source,
        title: msg,
        body: details,
        isTransient: true,
    });
    source.addNotification(notification);
}

/**
 * See shell_global_notify_problem().
 *
 * @param {string} msg - An error message
 * @param {string} details - Additional information
 */
export function notifyError(msg, details) {
    // Also print to stderr so it's logged somewhere
    if (details)
        console.warn(`error: ${msg}: ${details}`);
    else
        console.warn(`error: ${msg}`);

    notify(msg, details);
}

/**
 * @private
 * @param {Clutter.Grab} grab - grab
 */
function _findModal(grab) {
    for (let i = 0; i < modalActorFocusStack.length; i++) {
        if (modalActorFocusStack[i].grab === grab)
            return i;
    }
    return -1;
}

/**
 * Ensure we are in a mode where all keyboard and mouse input goes to
 * the stage, and focus @actor. Multiple calls to this function act in
 * a stacking fashion; the effect will be undone when an equal number
 * of popModal() invocations have been made.
 *
 * Next, record the current Clutter keyboard focus on a stack. If the
 * modal stack returns to this actor, reset the focus to the actor
 * which was focused at the time pushModal() was invoked.
 *
 * `params` may be used to provide the following parameters:
 *  - actionMode: used to set the current Shell.ActionMode to filter
 *                global keybindings; the default of NONE will filter
 *                out all keybindings
 *
 * @param {Clutter.Actor} actor - actor which will be given keyboard focus
 * @param {object=} params - optional parameters
 * @returns {Clutter.Grab} - the grab handle created
 */
export function pushModal(actor, params = {}) {
    const {actionMode: newActionMode} = {
        actionMode: Shell.ActionMode.NONE,
        ...params,
    };

    let grab = global.stage.grab(actor);

    if (modalCount === 0)
        Meta.disable_unredirect_for_display(global.display);

    modalCount += 1;
    let actorDestroyId = actor.connect('destroy', () => {
        let index = _findModal(grab);
        if (index >= 0)
            popModal(grab);
    });

    let prevFocus = global.stage.get_key_focus();
    let prevFocusDestroyId;
    if (prevFocus != null) {
        prevFocusDestroyId = prevFocus.connect('destroy', () => {
            const index = modalActorFocusStack.findIndex(
                record => record.prevFocus === prevFocus);

            if (index >= 0)
                modalActorFocusStack[index].prevFocus = null;
        });
    }
    modalActorFocusStack.push({
        actor,
        grab,
        destroyId: actorDestroyId,
        prevFocus,
        prevFocusDestroyId,
        actionMode,
    });

    actionMode = newActionMode;
    global.stage.set_key_focus(actor);
    return grab;
}

/**
 * Reverse the effect of pushModal(). If this invocation is undoing
 * the topmost invocation, then the focus will be restored to the
 * previous focus at the time when pushModal() was invoked.
 *
 * @param {Clutter.Grab} grab - the grab given by pushModal()
 */
export function popModal(grab) {
    let focusIndex = _findModal(grab);
    if (focusIndex < 0) {
        global.stage.set_key_focus(null);
        actionMode = Shell.ActionMode.NORMAL;

        throw new Error('incorrect pop');
    }

    modalCount -= 1;

    let record = modalActorFocusStack[focusIndex];
    record.actor.disconnect(record.destroyId);

    record.grab.dismiss();

    if (focusIndex === modalActorFocusStack.length - 1) {
        if (record.prevFocus)
            record.prevFocus.disconnect(record.prevFocusDestroyId);
        actionMode = record.actionMode;
        global.stage.set_key_focus(record.prevFocus);
    } else {
        // If we have:
        //     global.stage.set_focus(a);
        //     Main.pushModal(b);
        //     Main.pushModal(c);
        //     Main.pushModal(d);
        //
        // then we have the stack:
        //     [{ prevFocus: a, actor: b },
        //      { prevFocus: b, actor: c },
        //      { prevFocus: c, actor: d }]
        //
        // When actor c is destroyed/popped, if we only simply remove the
        // record, then the focus stack will be [a, c], rather than the correct
        // [a, b]. Shift the focus stack up before removing the record to ensure
        // that we get the correct result.
        let t = modalActorFocusStack[modalActorFocusStack.length - 1];
        if (t.prevFocus)
            t.prevFocus.disconnect(t.prevFocusDestroyId);
        // Remove from the middle, shift the focus chain up
        for (let i = modalActorFocusStack.length - 1; i > focusIndex; i--) {
            modalActorFocusStack[i].prevFocus = modalActorFocusStack[i - 1].prevFocus;
            modalActorFocusStack[i].prevFocusDestroyId = modalActorFocusStack[i - 1].prevFocusDestroyId;
            modalActorFocusStack[i].actionMode = modalActorFocusStack[i - 1].actionMode;
        }
    }
    modalActorFocusStack.splice(focusIndex, 1);

    if (modalCount > 0)
        return;

    layoutManager.modalEnded();
    Meta.enable_unredirect_for_display(global.display);
    actionMode = Shell.ActionMode.NORMAL;
}

/**
 * Creates the looking glass panel
 *
 * @returns {LookingGlass.LookingGlass}
 */
export function createLookingGlass() {
    if (lookingGlass == null)
        lookingGlass = new LookingGlass.LookingGlass();

    return lookingGlass;
}

/**
 * Opens the run dialog
 */
export function openRunDialog() {
    if (runDialog == null)
        runDialog = new RunDialog.RunDialog();

    runDialog.open();
}

export function openWelcomeDialog() {
    if (welcomeDialog === null)
        welcomeDialog = new WelcomeDialog.WelcomeDialog();

    welcomeDialog.open();
}

/**
 * activateWindow:
 *
 * @param {Meta.Window} window the window to activate
 * @param {number=} time current event time
 * @param {number=} workspaceNum  window's workspace number
 *
 * Activates @window, switching to its workspace first if necessary,
 * and switching out of the overview if it's currently active
 */
export function activateWindow(window, time, workspaceNum) {
    let workspaceManager = global.workspace_manager;
    let activeWorkspaceNum = workspaceManager.get_active_workspace_index();
    let windowWorkspaceNum = workspaceNum !== undefined ? workspaceNum : window.get_workspace().index();

    if (!time)
        time = global.get_current_time();

    if (windowWorkspaceNum !== activeWorkspaceNum) {
        let workspace = workspaceManager.get_workspace_by_index(windowWorkspaceNum);
        workspace.activate_with_focus(window, time);
    } else {
        window.activate(time);
    }

    overview.hide();
    panel.closeCalendar();
}

/**
 * Move @window to the specified monitor and workspace.
 *
 * @param {Meta.Window} window - the window to move
 * @param {number} monitorIndex - the requested monitor
 * @param {number} workspaceIndex - the requested workspace
 * @param {bool} append - create workspace if it doesn't exist
 */
export function moveWindowToMonitorAndWorkspace(window, monitorIndex, workspaceIndex, append = false) {
    // We need to move the window before changing the workspace, because
    // the move itself could cause a workspace change if the window enters
    // the primary monitor
    if (window.get_monitor() !== monitorIndex) {
        // Wait for the monitor change to take effect
        const id = global.display.connect('window-entered-monitor',
            (dsp, num, w) => {
                if (w !== window)
                    return;
                window.change_workspace_by_index(workspaceIndex, append);
                global.display.disconnect(id);
            });
        window.move_to_monitor(monitorIndex);
    } else {
        window.change_workspace_by_index(workspaceIndex, append);
    }
}

// TODO - replace this timeout with some system to guess when the user might
// be e.g. just reading the screen and not likely to interact.
const DEFERRED_TIMEOUT_SECONDS = 20;
let _deferredWorkData = {};
// Work scheduled for some point in the future
let _deferredWorkQueue = [];
// Work we need to process before the next redraw
let _beforeRedrawQueue = [];
// Counter to assign work ids
let _deferredWorkSequence = 0;
let _deferredTimeoutId = 0;

function _runDeferredWork(workId) {
    if (!_deferredWorkData[workId])
        return;
    let index = _deferredWorkQueue.indexOf(workId);
    if (index < 0)
        return;

    _deferredWorkQueue.splice(index, 1);
    _deferredWorkData[workId].callback();
    if (_deferredWorkQueue.length === 0 && _deferredTimeoutId > 0) {
        GLib.source_remove(_deferredTimeoutId);
        _deferredTimeoutId = 0;
    }
}

function _runAllDeferredWork() {
    while (_deferredWorkQueue.length > 0)
        _runDeferredWork(_deferredWorkQueue[0]);
}

function _runBeforeRedrawQueue() {
    for (let i = 0; i < _beforeRedrawQueue.length; i++) {
        let workId = _beforeRedrawQueue[i];
        _runDeferredWork(workId);
    }
    _beforeRedrawQueue = [];
}

function _queueBeforeRedraw(workId) {
    _beforeRedrawQueue.push(workId);
    if (_beforeRedrawQueue.length === 1) {
        const laters = global.compositor.get_laters();
        laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
            _runBeforeRedrawQueue();
            return false;
        });
    }
}

/**
 * This function sets up a callback to be invoked when either the
 * given actor is mapped, or after some period of time when the machine
 * is idle. This is useful if your actor isn't always visible on the
 * screen (for example, all actors in the overview), and you don't want
 * to consume resources updating if the actor isn't actually going to be
 * displaying to the user.
 *
 * Note that queueDeferredWork is called by default immediately on
 * initialization as well, under the assumption that new actors
 * will need it.
 *
 * @param {Clutter.Actor} actor - an actor
 * @param {callback} callback - Function to invoke to perform work
 *
 * @returns {string} - A string work identifier
 */
export function initializeDeferredWork(actor, callback) {
    // Turn into a string so we can use as an object property
    let workId = `${++_deferredWorkSequence}`;
    _deferredWorkData[workId] = {
        actor,
        callback,
    };
    actor.connect('notify::mapped', () => {
        if (!(actor.mapped && _deferredWorkQueue.includes(workId)))
            return;
        _queueBeforeRedraw(workId);
    });
    actor.connect('destroy', () => {
        let index = _deferredWorkQueue.indexOf(workId);
        if (index >= 0)
            _deferredWorkQueue.splice(index, 1);
        delete _deferredWorkData[workId];
    });
    queueDeferredWork(workId);
    return workId;
}

/**
 * queueDeferredWork:
 *
 * @param {string} workId work identifier
 *
 * Ensure that the work identified by @workId will be
 * run on map or timeout. You should call this function
 * for example when data being displayed by the actor has
 * changed.
 */
export function queueDeferredWork(workId) {
    let data = _deferredWorkData[workId];
    if (!data) {
        let message = `Invalid work id ${workId}`;
        logError(new Error(message), message);
        return;
    }
    if (!_deferredWorkQueue.includes(workId))
        _deferredWorkQueue.push(workId);
    if (data.actor.mapped) {
        _queueBeforeRedraw(workId);
    } else if (_deferredTimeoutId === 0) {
        _deferredTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, DEFERRED_TIMEOUT_SECONDS, () => {
            _runAllDeferredWork();
            _deferredTimeoutId = 0;
            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(_deferredTimeoutId, '[gnome-shell] _runAllDeferredWork');
    }
}

const RestartMessage = GObject.registerClass(
class RestartMessage extends ModalDialog.ModalDialog {
    _init(message) {
        super._init({
            shellReactive: true,
            styleClass: 'restart-message headline',
            shouldFadeIn: false,
            destroyOnClose: true,
        });

        let label = new St.Label({
            text: message,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });

        this.contentLayout.add_child(label);
        this.buttonLayout.hide();
    }
});

function showRestartMessage(message) {
    let restartMessage = new RestartMessage(message);
    restartMessage.open();
}

class AnimationsSettings {
    constructor() {
        this._animationsEnabled = true;
        this._handles = new Set();

        global.connect('notify::force-animations',
            this._syncAnimationsEnabled.bind(this));
        this._syncAnimationsEnabled();

        const backend = global.backend;
        const remoteAccessController = backend.get_remote_access_controller();
        if (remoteAccessController) {
            remoteAccessController.connect('new-handle',
                (_, handle) => this._onNewRemoteAccessHandle(handle));
        }
    }

    _shouldEnableAnimations() {
        if (this._handles.size > 0)
            return false;

        if (global.force_animations)
            return true;

        const backend = global.backend;
        if (!backend.is_rendering_hardware_accelerated())
            return false;

        if (Shell.util_has_x11_display_extension(
            global.display, 'VNC-EXTENSION'))
            return false;

        return true;
    }

    _syncAnimationsEnabled() {
        const shouldEnableAnimations = this._shouldEnableAnimations();
        if (this._animationsEnabled === shouldEnableAnimations)
            return;
        this._animationsEnabled = shouldEnableAnimations;

        const settings = St.Settings.get();
        if (shouldEnableAnimations)
            settings.uninhibit_animations();
        else
            settings.inhibit_animations();
    }

    _onRemoteAccessHandleStopped(handle) {
        this._handles.delete(handle);
        this._syncAnimationsEnabled();
    }

    _onNewRemoteAccessHandle(handle) {
        if (!handle.get_disable_animations())
            return;

        this._handles.add(handle);
        this._syncAnimationsEnabled();
        handle.connect('stopped', this._onRemoteAccessHandleStopped.bind(this));
    }
}
(uuay)dependencies.jsimport gi from 'gi';

/**
 * Required dependencies
 */

import 'gi://AccountsService?version=1.0';
import 'gi://Atk?version=1.0';
import 'gi://Atspi?version=2.0';
import 'gi://Gcr?version=4';
import 'gi://Gdk?version=4.0';
import 'gi://Gdm?version=1.0';
import 'gi://Geoclue?version=2.0';
import 'gi://Gio?version=2.0';
import 'gi://GDesktopEnums?version=3.0';
import 'gi://GdkPixbuf?version=2.0';
import 'gi://GnomeBG?version=4.0';
import 'gi://GnomeDesktop?version=4.0';
import 'gi://Graphene?version=1.0';
import 'gi://GWeather?version=4.0';
import 'gi://IBus?version=1.0';
import 'gi://Pango?version=1.0';
import 'gi://Polkit?version=1.0';
import 'gi://PolkitAgent?version=1.0';
import 'gi://Rsvg?version=2.0';
import 'gi://Soup?version=3.0';
import 'gi://UPowerGlib?version=1.0';

import * as Config from './config.js';

// Meta-related dependencies use a shared version
// from the compile-time config.
gi.require('Meta', Config.LIBMUTTER_API_VERSION);
gi.require('Clutter', Config.LIBMUTTER_API_VERSION);
gi.require('Cogl', Config.LIBMUTTER_API_VERSION);
gi.require('Shell', Config.LIBMUTTER_API_VERSION);
gi.require('St', Config.LIBMUTTER_API_VERSION);

/**
 * Compile-time optional dependencies
 */

if (Config.HAVE_BLUETOOTH)
    gi.require('GnomeBluetooth', '3.0');
else
    console.debug('GNOME Shell was compiled without GNOME Bluetooth support');


if (Config.HAVE_NETWORKMANAGER) {
    gi.require('NM', '1.0');
    gi.require('NMA4', '1.0');
} else {
    console.debug('GNOME Shell was compiled without Network Manager support');
}

/**
 * Runtime optional dependencies
 */

try {
    // Malcontent is optional, so catch any errors loading it
    gi.require('Malcontent', '0');
} catch {
    console.debug('Malcontent is not available, parental controls integration will be disabled.');
}
(uuay)sessionMode.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GLib from 'gi://GLib';
import * as Signals from '../misc/signals.js';

import * as FileUtils from '../misc/fileUtils.js';

import {LoginDialog}  from '../gdm/loginDialog.js';
import {UnlockDialog} from '../ui/unlockDialog.js';

import * as Config from '../misc/config.js';

const DEFAULT_MODE = 'restrictive';

const USER_SESSION_COMPONENTS = [
    'polkitAgent', 'keyring',
    'autorunManager', 'automountManager',
];

if (Config.HAVE_NETWORKMANAGER)
    USER_SESSION_COMPONENTS.push('networkAgent');

const _modes = {
    'restrictive': {
        parentMode: null,
        stylesheetName: 'gnome-shell.css',
        colorScheme: 'prefer-dark',
        themeResourceName: 'gnome-shell-theme.gresource',
        iconsResourceName: 'gnome-shell-icons.gresource',
        hasOverview: false,
        showCalendarEvents: false,
        showWelcomeDialog: false,
        allowSettings: false,
        allowScreencast: false,
        debugFlags: [],
        enabledExtensions: [],
        hasRunDialog: false,
        hasWorkspaces: false,
        hasWindows: false,
        hasNotifications: false,
        hasWmMenus: false,
        isLocked: false,
        isGreeter: false,
        isPrimary: false,
        unlockDialog: null,
        components: [],
        panel: {
            left: [],
            center: [],
            right: [],
        },
        panelStyle: null,
    },

    'gdm': {
        hasNotifications: true,
        stylesheetName: 'gdm.css',
        themeResourceName: 'gdm-theme.gresource',
        isGreeter: true,
        isPrimary: true,
        unlockDialog: LoginDialog,
        components: Config.HAVE_NETWORKMANAGER
            ? ['networkAgent', 'polkitAgent']
            : ['polkitAgent'],
        panel: {
            left: [],
            center: ['dateMenu'],
            right: ['dwellClick', 'a11y', 'keyboard', 'quickSettings'],
        },
        panelStyle: 'login-screen',
    },

    'unlock-dialog': {
        isLocked: true,
        unlockDialog: undefined,
        components: ['polkitAgent'],
        panel: {
            left: [],
            center: [],
            right: ['dwellClick', 'a11y', 'keyboard', 'quickSettings'],
        },
        panelStyle: 'unlock-screen',
    },

    'user': {
        hasOverview: true,
        showCalendarEvents: true,
        showWelcomeDialog: true,
        allowSettings: true,
        allowScreencast: true,
        hasRunDialog: true,
        hasWorkspaces: true,
        hasWindows: true,
        hasWmMenus: true,
        hasNotifications: true,
        isLocked: false,
        isPrimary: true,
        unlockDialog: UnlockDialog,
        components: USER_SESSION_COMPONENTS,
        panel: {
            left: ['activities'],
            center: ['dateMenu'],
            right: ['screenRecording', 'screenSharing', 'dwellClick', 'a11y', 'keyboard', 'quickSettings'],
        },
    },
};

function _loadMode(file, info) {
    let name = info.get_name();
    let suffix = name.indexOf('.json');
    let modeName = suffix === -1 ? name : name.slice(name, suffix);

    if (Object.prototype.hasOwnProperty.call(_modes, modeName))
        return;

    let fileContent, success_, newMode;
    try {
        [success_, fileContent] = file.load_contents(null);
        const decoder = new TextDecoder();
        newMode = JSON.parse(decoder.decode(fileContent));
    } catch (e) {
        return;
    }

    _modes[modeName] = {};
    const  excludedProps = ['unlockDialog'];
    for (let prop in _modes[DEFAULT_MODE]) {
        if (newMode[prop] !== undefined &&
            !excludedProps.includes(prop))
            _modes[modeName][prop] = newMode[prop];
    }
    _modes[modeName]['isPrimary'] = true;
}

/**
 * Loads external session modes from the system data directories.
 */
function _loadModes() {
    for (const {dir, info} of FileUtils.collectFromDatadirs('modes', false))
        _loadMode(dir, info);
}

export function listModes() {
    _loadModes();
    let loop = new GLib.MainLoop(null, false);
    let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
        let names = Object.getOwnPropertyNames(_modes);
        for (let i = 0; i < names.length; i++) {
            if (_modes[names[i]].isPrimary)
                print(names[i]);
        }
        loop.quit();
    });
    GLib.Source.set_name_by_id(id, '[gnome-shell] listModes');
    loop.run();
}

export class SessionMode extends Signals.EventEmitter {
    constructor() {
        super();

        _loadModes();
        let isPrimary = _modes[global.session_mode] &&
                         _modes[global.session_mode].isPrimary;
        let mode = isPrimary ? global.session_mode : 'user';
        this._modeStack = [mode];
        this._sync();
    }

    pushMode(mode) {
        console.debug(`sessionMode: Pushing mode ${mode}`);
        this._modeStack.push(mode);
        this._sync();
    }

    popMode(mode) {
        if (this.currentMode !== mode || this._modeStack.length === 1)
            throw new Error('Invalid SessionMode.popMode');

        console.debug(`sessionMode: Popping mode ${mode}`);
        this._modeStack.pop();
        this._sync();
    }

    switchMode(to) {
        if (this.currentMode === to)
            return;
        this._modeStack[this._modeStack.length - 1] = to;
        this._sync();
    }

    get currentMode() {
        return this._modeStack[this._modeStack.length - 1];
    }

    _sync() {
        const current = _modes[this.currentMode];
        const parent = current.parentMode
            ? {..._modes[DEFAULT_MODE], ..._modes[current.parentMode]}
            : {..._modes[DEFAULT_MODE]};

        const params = {...parent, ...current};

        // A simplified version of Lang.copyProperties, handles
        // undefined as a special case for "no change / inherit from previous mode"
        for (let prop in params) {
            if (params[prop] !== undefined)
                this[prop] = params[prop];
        }

        this.emit('updated');
    }
}
(uuay)params.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

/**
 * parse:
 *
 * @param {*} params caller-provided parameter object, or %null
 * @param {*} defaults provided defaults object
 * @param {boolean} [allowExtras] whether or not to allow properties not in `default`
 *
 * @summary Examines `params` and fills in default values from `defaults` for
 * any properties in `default` that don't appear in `params`. If
 * `allowExtras` is not %true, it will throw an error if `params`
 * contains any properties that aren't in `defaults`.
 *
 * If `params` is %null, this returns the values from `defaults`.
 *
 * @returns a new object, containing the merged parameters from
 * `params` and `defaults`
 */
export function parse(params = {}, defaults, allowExtras) {
    if (!allowExtras) {
        for (let prop in params) {
            if (!(prop in defaults))
                throw new Error(`Unrecognized parameter "${prop}"`);
        }
    }

    return {...defaults, ...params};
}
(uuay)background.jsr// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

// READ THIS FIRST
// Background handling is a maze of objects, both objects in this file, and
// also objects inside Mutter. They all have a role.
//
// BackgroundManager
//   The only object that other parts of GNOME Shell deal with; a
//   BackgroundManager creates background actors and adds them to
//   the specified container. When the background is changed by the
//   user it will fade out the old actor and fade in the new actor.
//   (This is separate from the fading for an animated background,
//   since using two actors is quite inefficient.)
//
// MetaBackgroundImage
//   An object represented an image file that will be used for drawing
//   the background. MetaBackgroundImage objects asynchronously load,
//   so they are first created in an unloaded state, then later emit
//   a ::loaded signal when the Cogl object becomes available.
//
// MetaBackgroundImageCache
//   A cache from filename to MetaBackgroundImage.
//
// BackgroundSource
//   An object that is created for each GSettings schema (separate
//   settings schemas are used for the lock screen and main background),
//   and holds a reference to shared Background objects.
//
// MetaBackground
//   Holds the specification of a background - a background color
//   or gradient and one or two images blended together.
//
// Background
//   JS delegate object that Connects a MetaBackground to the GSettings
//   schema for the background.
//
// Animation
//   A helper object that handles loading a XML-based animation; it is a
//   wrapper for GnomeDesktop.BGSlideShow
//
// MetaBackgroundActor
//   An actor that draws the background for a single monitor
//
// BackgroundCache
//   A cache of Settings schema => BackgroundSource and of a single Animation.
//   Also used to share file monitors.
//
// A static image, background color or gradient is relatively straightforward. The
// calling code creates a separate BackgroundManager for each monitor. Since they
// are created for the same GSettings schema, they will use the same BackgroundSource
// object, which provides a single Background and correspondingly a single
// MetaBackground object.
//
// BackgroundManager               BackgroundManager
//        |        \               /        |
//        |         BackgroundSource        |        looked up in BackgroundCache
//        |                |                |
//        |            Background           |
//        |                |                |
//   MetaBackgroundActor   |    MetaBackgroundActor
//         \               |               /
//          `------- MetaBackground ------'
//                         |
//                MetaBackgroundImage            looked up in MetaBackgroundImageCache
//
// The animated case is tricker because the animation XML file can specify different
// files for different monitor resolutions and aspect ratios. For this reason,
// the BackgroundSource provides different Background share a single Animation object,
// which tracks the animation, but use different MetaBackground objects. In the
// common case, the different MetaBackground objects will be created for the
// same filename and look up the *same* MetaBackgroundImage object, so there is
// little wasted memory:
//
// BackgroundManager               BackgroundManager
//        |        \               /        |
//        |         BackgroundSource        |        looked up in BackgroundCache
//        |             /      \            |
//        |     Background   Background     |
//        |       |     \      /   |        |
//        |       |    Animation   |        |        looked up in BackgroundCache
// MetaBackgroundA|tor           Me|aBackgroundActor
//         \      |                |       /
//      MetaBackground           MetaBackground
//                 \                 /
//                MetaBackgroundImage            looked up in MetaBackgroundImageCache
//                MetaBackgroundImage
//
// But the case of different filenames and different background images
// is possible as well:
//                        ....
//      MetaBackground              MetaBackground
//             |                          |
//     MetaBackgroundImage         MetaBackgroundImage
//     MetaBackgroundImage         MetaBackgroundImage

import Clutter from 'gi://Clutter';
import GDesktopEnums from 'gi://GDesktopEnums';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import GnomeBG from 'gi://GnomeBG';
import GnomeDesktop from 'gi://GnomeDesktop';
import Meta from 'gi://Meta';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';

import * as Desktop from '../misc/desktop.js';
import * as LoginManager from '../misc/loginManager.js';
import * as Main from './main.js';
import * as Params from '../misc/params.js';

const DEFAULT_BACKGROUND_COLOR = Clutter.Color.from_pixel(0x2e3436ff);

const BACKGROUND_SCHEMA = 'org.gnome.desktop.background';
const PRIMARY_COLOR_KEY = 'primary-color';
const SECONDARY_COLOR_KEY = 'secondary-color';
const COLOR_SHADING_TYPE_KEY = 'color-shading-type';
const BACKGROUND_STYLE_KEY = 'picture-options';
const PICTURE_URI_KEY = 'picture-uri';
const PICTURE_URI_DARK_KEY = 'picture-uri-dark';

const INTERFACE_SCHEMA = 'org.gnome.desktop.interface';
const COLOR_SCHEME_KEY = 'color-scheme';

const FADE_ANIMATION_TIME = 1000;

// These parameters affect how often we redraw.
// The first is how different (percent crossfaded) the slide show
// has to look before redrawing and the second is the minimum
// frequency (in seconds) we're willing to wake up
const ANIMATION_OPACITY_STEP_INCREMENT = 4.0;
const ANIMATION_MIN_WAKEUP_INTERVAL = 1.0;

let _backgroundCache = null;

function _fileEqual0(file1, file2) {
    if (file1 === file2)
        return true;

    if (!file1 || !file2)
        return false;

    return file1.equal(file2);
}

class BackgroundCache extends Signals.EventEmitter {
    constructor() {
        super();

        this._fileMonitors = {};
        this._backgroundSources = {};
        this._animations = {};
    }

    monitorFile(file) {
        let key = file.hash();
        if (this._fileMonitors[key])
            return;

        let monitor = file.monitor(Gio.FileMonitorFlags.NONE, null);
        monitor.connect('changed',
            (obj, theFile, otherFile, eventType) => {
                // Ignore CHANGED and CREATED events, since in both cases
                // we'll get a CHANGES_DONE_HINT event when done.
                if (eventType !== Gio.FileMonitorEvent.CHANGED &&
                    eventType !== Gio.FileMonitorEvent.CREATED)
                    this.emit('file-changed', file);
            });

        this._fileMonitors[key] = monitor;
    }

    getAnimation(params) {
        params = Params.parse(params, {
            file: null,
            settingsSchema: null,
            onLoaded: null,
        });

        let animation = this._animations[params.settingsSchema];
        if (animation && _fileEqual0(animation.file, params.file)) {
            if (params.onLoaded) {
                let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                    params.onLoaded(this._animations[params.settingsSchema]);
                    return GLib.SOURCE_REMOVE;
                });
                GLib.Source.set_name_by_id(id, '[gnome-shell] params.onLoaded');
            }
            return;
        }

        animation = new Animation({file: params.file});

        animation.load_async(null, () => {
            this._animations[params.settingsSchema] = animation;

            if (params.onLoaded) {
                let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                    params.onLoaded(this._animations[params.settingsSchema]);
                    return GLib.SOURCE_REMOVE;
                });
                GLib.Source.set_name_by_id(id, '[gnome-shell] params.onLoaded');
            }
        });
    }

    getBackgroundSource(layoutManager, settingsSchema) {
        // The layoutManager is always the same one; we pass in it since
        // Main.layoutManager may not be set yet

        if (!(settingsSchema in this._backgroundSources)) {
            this._backgroundSources[settingsSchema] = new BackgroundSource(layoutManager, settingsSchema);
            this._backgroundSources[settingsSchema]._useCount = 1;
        } else {
            this._backgroundSources[settingsSchema]._useCount++;
        }

        return this._backgroundSources[settingsSchema];
    }

    releaseBackgroundSource(settingsSchema) {
        if (settingsSchema in this._backgroundSources) {
            let source = this._backgroundSources[settingsSchema];
            source._useCount--;
            if (source._useCount === 0) {
                delete this._backgroundSources[settingsSchema];
                source.destroy();
            }
        }
    }
}

/**
 * @returns {BackgroundCache}
 */
function getBackgroundCache() {
    if (!_backgroundCache)
        _backgroundCache = new BackgroundCache();
    return _backgroundCache;
}

const Background = GObject.registerClass({
    Signals: {'loaded': {}, 'bg-changed': {}},
}, class Background extends Meta.Background {
    _init(params) {
        params = Params.parse(params, {
            monitorIndex: 0,
            layoutManager: Main.layoutManager,
            settings: null,
            file: null,
            style: null,
        });

        super._init({meta_display: global.display});

        this._settings = params.settings;
        this._file = params.file;
        this._style = params.style;
        this._monitorIndex = params.monitorIndex;
        this._layoutManager = params.layoutManager;
        this._fileWatches = {};
        this._cancellable = new Gio.Cancellable();
        this.isLoaded = false;

        this._interfaceSettings = new Gio.Settings({schema_id: INTERFACE_SCHEMA});

        this._clock = new GnomeDesktop.WallClock();
        this._clock.connectObject('notify::timezone',
            () => {
                if (this._animation)
                    this._loadAnimation(this._animation.file);
            }, this);

        let loginManager = LoginManager.getLoginManager();
        loginManager.connectObject('prepare-for-sleep',
            (lm, aboutToSuspend) => {
                if (aboutToSuspend)
                    return;
                this._refreshAnimation();
            }, this);

        this._settings.connectObject('changed',
            this._emitChangedSignal.bind(this), this);

        this._interfaceSettings.connectObject(`changed::${COLOR_SCHEME_KEY}`,
            this._emitChangedSignal.bind(this), this);

        this._load();
    }

    destroy() {
        this._cancellable.cancel();
        this._removeAnimationTimeout();

        let i;
        let keys = Object.keys(this._fileWatches);
        for (i = 0; i < keys.length; i++)
            this._cache.disconnect(this._fileWatches[keys[i]]);

        this._fileWatches = null;

        this._clock.disconnectObject(this);
        this._clock = null;

        LoginManager.getLoginManager().disconnectObject(this);
        this._settings.disconnectObject(this);
        this._interfaceSettings.disconnectObject(this);

        if (this._changedIdleId) {
            GLib.source_remove(this._changedIdleId);
            this._changedIdleId = 0;
        }
    }

    _emitChangedSignal() {
        if (this._changedIdleId)
            return;

        this._changedIdleId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
            this._changedIdleId = 0;
            this.emit('bg-changed');
            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(this._changedIdleId,
            '[gnome-shell] Background._emitChangedSignal');
    }

    updateResolution() {
        if (this._animation)
            this._refreshAnimation();
    }

    _refreshAnimation() {
        if (!this._animation)
            return;

        this._removeAnimationTimeout();
        this._updateAnimation();
    }

    _setLoaded() {
        if (this.isLoaded)
            return;

        this.isLoaded = true;
        if (this._cancellable?.is_cancelled())
            return;

        let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
            this.emit('loaded');
            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(id, '[gnome-shell] Background._setLoaded Idle');
    }

    _loadPattern() {
        let colorString, res_, color, secondColor;

        colorString = this._settings.get_string(PRIMARY_COLOR_KEY);
        [res_, color] = Clutter.Color.from_string(colorString);
        colorString = this._settings.get_string(SECONDARY_COLOR_KEY);
        [res_, secondColor] = Clutter.Color.from_string(colorString);

        let shadingType = this._settings.get_enum(COLOR_SHADING_TYPE_KEY);

        if (shadingType === GDesktopEnums.BackgroundShading.SOLID)
            this.set_color(color);
        else
            this.set_gradient(shadingType, color, secondColor);
    }

    _watchFile(file) {
        let key = file.hash();
        if (this._fileWatches[key])
            return;

        this._cache.monitorFile(file);
        let signalId = this._cache.connect('file-changed',
            (cache, changedFile) => {
                if (changedFile.equal(file)) {
                    let imageCache = Meta.BackgroundImageCache.get_default();
                    imageCache.purge(changedFile);
                    this._emitChangedSignal();
                }
            });
        this._fileWatches[key] = signalId;
    }

    _removeAnimationTimeout() {
        if (this._updateAnimationTimeoutId) {
            GLib.source_remove(this._updateAnimationTimeoutId);
            this._updateAnimationTimeoutId = 0;
        }
    }

    _updateAnimation() {
        this._updateAnimationTimeoutId = 0;

        this._animation.update(this._layoutManager.monitors[this._monitorIndex]);
        let files = this._animation.keyFrameFiles;

        let finish = () => {
            this._setLoaded();
            if (files.length > 1) {
                this.set_blend(files[0], files[1],
                    this._animation.transitionProgress,
                    this._style);
            } else if (files.length > 0) {
                this.set_file(files[0], this._style);
            } else {
                this.set_file(null, this._style);
            }
            this._queueUpdateAnimation();
        };

        let cache = Meta.BackgroundImageCache.get_default();
        let numPendingImages = files.length;
        for (let i = 0; i < files.length; i++) {
            this._watchFile(files[i]);
            let image = cache.load(files[i]);
            if (image.is_loaded()) {
                numPendingImages--;
                if (numPendingImages === 0)
                    finish();
            } else {
                // eslint-disable-next-line no-loop-func
                let id = image.connect('loaded', () => {
                    image.disconnect(id);
                    numPendingImages--;
                    if (numPendingImages === 0)
                        finish();
                });
            }
        }
    }

    _queueUpdateAnimation() {
        if (this._updateAnimationTimeoutId !== 0)
            return;

        if (!this._cancellable || this._cancellable.is_cancelled())
            return;

        if (!this._animation.transitionDuration)
            return;

        let nSteps = 255 / ANIMATION_OPACITY_STEP_INCREMENT;
        let timePerStep = (this._animation.transitionDuration * 1000) / nSteps;

        let interval = Math.max(
            ANIMATION_MIN_WAKEUP_INTERVAL * 1000,
            timePerStep);

        if (interval > GLib.MAXUINT32)
            return;

        this._updateAnimationTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
            interval,
            () => {
                this._updateAnimationTimeoutId = 0;
                this._updateAnimation();
                return GLib.SOURCE_REMOVE;
            });
        GLib.Source.set_name_by_id(this._updateAnimationTimeoutId, '[gnome-shell] this._updateAnimation');
    }

    _loadAnimation(file) {
        this._cache.getAnimation({
            file,
            settingsSchema: this._settings.schema_id,
            onLoaded: animation => {
                this._animation = animation;

                if (!this._animation || this._cancellable.is_cancelled()) {
                    this._setLoaded();
                    return;
                }

                this._updateAnimation();
                this._watchFile(file);
            },
        });
    }

    _loadImage(file) {
        this.set_file(file, this._style);
        this._watchFile(file);

        let cache = Meta.BackgroundImageCache.get_default();
        let image = cache.load(file);
        if (image.is_loaded()) {
            this._setLoaded();
        } else {
            let id = image.connect('loaded', () => {
                this._setLoaded();
                image.disconnect(id);
            });
        }
    }

    async _loadFile(file) {
        let info;
        try {
            info = await file.query_info_async(
                Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
                Gio.FileQueryInfoFlags.NONE,
                0,
                this._cancellable);
        } catch (e) {
            this._setLoaded();
            return;
        }

        const contentType = info.get_content_type();
        if (contentType === 'application/xml')
            this._loadAnimation(file);
        else
            this._loadImage(file);
    }

    _load() {
        this._cache = getBackgroundCache();

        this._loadPattern();

        if (!this._file) {
            this._setLoaded();
            return;
        }

        this._loadFile(this._file);
    }
});

let _systemBackground;

export const SystemBackground = GObject.registerClass({
    Signals: {'loaded': {}},
}, class SystemBackground extends Meta.BackgroundActor {
    _init() {
        if (_systemBackground == null) {
            _systemBackground = new Meta.Background({meta_display: global.display});

            let backgroundColor = DEFAULT_BACKGROUND_COLOR;
            if (Desktop.is('ubuntu')) {
                const loginSettings = new Gio.Settings({schema_id: 'com.ubuntu.login-screen'});
                const bgColor = loginSettings.get_string('background-color');
                const dummyBgActor = new St.Widget({name: 'lockDialogGroup'});
                if (bgColor)
                    dummyBgActor.set_style(`background-color: ${bgColor};`);
                Main.uiGroup.add_child(dummyBgActor);
                backgroundColor = dummyBgActor.get_theme_node().get_background_color();
                dummyBgActor.destroy();
            }

            _systemBackground.set_color(backgroundColor);
        }

        super._init({
            meta_display: global.display,
            monitor: 0,
        });
        this.content.background = _systemBackground;

        let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
            this.emit('loaded');
            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(id, '[gnome-shell] SystemBackground.loaded');
    }
});

class BackgroundSource {
    constructor(layoutManager, settingsSchema) {
        // Allow override the background image setting for performance testing
        this._layoutManager = layoutManager;
        this._overrideImage = GLib.getenv('SHELL_BACKGROUND_IMAGE');
        this._settings = new Gio.Settings({schema_id: settingsSchema});
        this._backgrounds = [];

        const monitorManager = global.backend.get_monitor_manager();
        this._monitorsChangedId =
            monitorManager.connect('monitors-changed',
                this._onMonitorsChanged.bind(this));

        this._interfaceSettings = new Gio.Settings({schema_id: INTERFACE_SCHEMA});
    }

    _onMonitorsChanged() {
        for (let monitorIndex in this._backgrounds) {
            let background = this._backgrounds[monitorIndex];

            if (monitorIndex < this._layoutManager.monitors.length) {
                background.updateResolution();
            } else {
                background.disconnect(background._changedId);
                background.destroy();
                delete this._backgrounds[monitorIndex];
            }
        }
    }

    getBackground(monitorIndex) {
        let file = null;
        let style;

        // We don't watch changes to settings here,
        // instead we rely on Background to watch those
        // and emit 'bg-changed' at the right time

        if (this._overrideImage != null) {
            file = Gio.File.new_for_path(this._overrideImage);
            style = GDesktopEnums.BackgroundStyle.ZOOM; // Hardcode
        } else {
            style = this._settings.get_enum(BACKGROUND_STYLE_KEY);
            if (style !== GDesktopEnums.BackgroundStyle.NONE) {
                const colorScheme = this._interfaceSettings.get_enum('color-scheme');
                const uri = this._settings.get_string(
                    colorScheme === GDesktopEnums.ColorScheme.PREFER_DARK
                        ? PICTURE_URI_DARK_KEY
                        : PICTURE_URI_KEY);

                file = Gio.File.new_for_commandline_arg(uri);
            }
        }

        // Animated backgrounds are (potentially) per-monitor, since
        // they can have variants that depend on the aspect ratio and
        // size of the monitor; for other backgrounds we can use the
        // same background object for all monitors.
        if (file == null || !file.get_basename().endsWith('.xml'))
            monitorIndex = 0;

        if (!(monitorIndex in this._backgrounds)) {
            let background = new Background({
                monitorIndex,
                layoutManager: this._layoutManager,
                settings: this._settings,
                file,
                style,
            });

            background._changedId = background.connect('bg-changed', () => {
                background.disconnect(background._changedId);
                background.destroy();
                delete this._backgrounds[monitorIndex];
            });

            this._backgrounds[monitorIndex] = background;
        }

        return this._backgrounds[monitorIndex];
    }

    destroy() {
        const monitorManager = global.backend.get_monitor_manager();
        monitorManager.disconnect(this._monitorsChangedId);

        for (let monitorIndex in this._backgrounds) {
            let background = this._backgrounds[monitorIndex];
            background.disconnect(background._changedId);
            background.destroy();
        }

        this._backgrounds = null;
    }
}

const Animation = GObject.registerClass(
class Animation extends GnomeBG.BGSlideShow {
    _init(params) {
        super._init(params);

        this.keyFrameFiles = [];
        this.transitionProgress = 0.0;
        this.transitionDuration = 0.0;
        this.loaded = false;
    }

    // eslint-disable-next-line camelcase
    load_async(cancellable, callback) {
        super.load_async(cancellable, () => {
            this.loaded = true;

            callback?.();
        });
    }

    update(monitor) {
        this.keyFrameFiles = [];

        if (this.get_num_slides() < 1)
            return;

        let [progress, duration, isFixed_, filename1, filename2] =
            this.get_current_slide(monitor.width, monitor.height);

        this.transitionDuration = duration;
        this.transitionProgress = progress;

        if (filename1)
            this.keyFrameFiles.push(Gio.File.new_for_path(filename1));

        if (filename2)
            this.keyFrameFiles.push(Gio.File.new_for_path(filename2));
    }
});

export class BackgroundManager extends Signals.EventEmitter {
    constructor(params) {
        super();
        params = Params.parse(params, {
            container: null,
            layoutManager: Main.layoutManager,
            monitorIndex: null,
            vignette: false,
            controlPosition: true,
            settingsSchema: BACKGROUND_SCHEMA,
            useContentSize: true,
        });

        let cache = getBackgroundCache();
        this._settingsSchema = params.settingsSchema;
        this._backgroundSource = cache.getBackgroundSource(params.layoutManager, params.settingsSchema);

        this._container = params.container;
        this._layoutManager = params.layoutManager;
        this._vignette = params.vignette;
        this._monitorIndex = params.monitorIndex;
        this._controlPosition = params.controlPosition;
        this._useContentSize = params.useContentSize;

        this.backgroundActor = this._createBackgroundActor();
        this._newBackgroundActor = null;
    }

    destroy() {
        let cache = getBackgroundCache();
        cache.releaseBackgroundSource(this._settingsSchema);
        this._backgroundSource = null;

        if (this._newBackgroundActor) {
            this._newBackgroundActor.destroy();
            this._newBackgroundActor = null;
        }

        if (this.backgroundActor) {
            this.backgroundActor.destroy();
            this.backgroundActor = null;
        }
    }

    _swapBackgroundActor() {
        let oldBackgroundActor = this.backgroundActor;
        this.backgroundActor = this._newBackgroundActor;
        this._newBackgroundActor = null;
        this.emit('changed');

        if (Main.layoutManager.screenTransition.visible) {
            oldBackgroundActor.destroy();
            return;
        }

        oldBackgroundActor.ease({
            opacity: 0,
            duration: FADE_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => oldBackgroundActor.destroy(),
        });
    }

    _updateBackgroundActor() {
        if (this._newBackgroundActor) {
            /* Skip displaying existing background queued for load */
            this._newBackgroundActor.destroy();
            this._newBackgroundActor = null;
        }

        let newBackgroundActor = this._createBackgroundActor();

        const oldContent = this.backgroundActor.content;
        const newContent = newBackgroundActor.content;

        newContent.vignette_sharpness = oldContent.vignette_sharpness;
        newContent.brightness = oldContent.brightness;

        newBackgroundActor.visible = this.backgroundActor.visible;

        this._newBackgroundActor = newBackgroundActor;

        const {background} = newBackgroundActor.content;

        if (background.isLoaded) {
            this._swapBackgroundActor();
        } else {
            newBackgroundActor.loadedSignalId = background.connect('loaded',
                () => {
                    background.disconnect(newBackgroundActor.loadedSignalId);
                    newBackgroundActor.loadedSignalId = 0;

                    this._swapBackgroundActor();
                });
        }
    }

    _createBackgroundActor() {
        let background = this._backgroundSource.getBackground(this._monitorIndex);
        let backgroundActor = new Meta.BackgroundActor({
            meta_display: global.display,
            monitor: this._monitorIndex,
            request_mode: this._useContentSize
                ? Clutter.RequestMode.CONTENT_SIZE
                : Clutter.RequestMode.HEIGHT_FOR_WIDTH,
            x_expand: !this._useContentSize,
            y_expand: !this._useContentSize,
        });
        backgroundActor.content.set({
            background,
            vignette: this._vignette,
            vignette_sharpness: 0.5,
            brightness: 0.5,
        });

        this._container.add_child(backgroundActor);

        if (this._controlPosition) {
            let monitor = this._layoutManager.monitors[this._monitorIndex];
            backgroundActor.set_position(monitor.x, monitor.y);
            this._container.set_child_below_sibling(backgroundActor, null);
        }

        let changeSignalId = background.connect('bg-changed', () => {
            background.disconnect(changeSignalId);
            changeSignalId = null;
            this._updateBackgroundActor();
        });

        let loadedSignalId;
        if (background.isLoaded) {
            GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                this.emit('loaded');
                return GLib.SOURCE_REMOVE;
            });
        } else {
            loadedSignalId = background.connect('loaded', () => {
                background.disconnect(loadedSignalId);
                loadedSignalId = null;
                this.emit('loaded');
            });
        }

        backgroundActor.connect('destroy', () => {
            if (changeSignalId)
                background.disconnect(changeSignalId);

            if (loadedSignalId)
                background.disconnect(loadedSignalId);

            if (backgroundActor.loadedSignalId)
                background.disconnect(backgroundActor.loadedSignalId);
        });

        return backgroundActor;
    }
}
(uuay)xdndHandler.jsw// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import * as Signals from '../misc/signals.js';

import * as DND from './dnd.js';
import * as Main from './main.js';

export class XdndHandler extends Signals.EventEmitter {
    constructor() {
        super();

        // Used to display a clone of the cursor window when the
        // window group is hidden (like it happens in the overview)
        this._cursorWindowClone = null;

        // Used as a drag actor in case we don't have a cursor window clone
        this._dummy = new Clutter.Actor({width: 1, height: 1, opacity: 0});
        Main.uiGroup.add_child(this._dummy);
        this._dummy.hide();

        var dnd = global.backend.get_dnd();
        dnd.connect('dnd-enter', this._onEnter.bind(this));
        dnd.connect('dnd-position-change', this._onPositionChanged.bind(this));
        dnd.connect('dnd-leave', this._onLeave.bind(this));
    }

    // Called when the user cancels the drag (i.e release the button)
    _onLeave() {
        global.window_group.disconnectObject(this);
        if (this._cursorWindowClone) {
            this._cursorWindowClone.destroy();
            this._cursorWindowClone = null;
        }

        this.emit('drag-end');
    }

    _onEnter() {
        global.window_group.connectObject('notify::visible',
            this._onWindowGroupVisibilityChanged.bind(this), this);

        this.emit('drag-begin', global.get_current_time());
    }

    _onWindowGroupVisibilityChanged() {
        if (!global.window_group.visible) {
            if (this._cursorWindowClone)
                return;

            let windows = global.get_window_actors();
            let cursorWindow = windows[windows.length - 1];

            // FIXME: more reliable way?
            if (!cursorWindow.get_meta_window().is_override_redirect())
                return;

            const constraintPosition = new Clutter.BindConstraint({
                coordinate: Clutter.BindCoordinate.POSITION,
                source: cursorWindow,
            });

            this._cursorWindowClone = new Clutter.Clone({source: cursorWindow});
            Main.uiGroup.add_child(this._cursorWindowClone);

            // Make sure that the clone has the same position as the source
            this._cursorWindowClone.add_constraint(constraintPosition);
        } else {
            if (!this._cursorWindowClone)
                return;

            this._cursorWindowClone.destroy();
            this._cursorWindowClone = null;
        }
    }

    _onPositionChanged(obj, x, y) {
        let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);

        // Make sure that the cursor window is on top
        if (this._cursorWindowClone)
            Main.uiGroup.set_child_above_sibling(this._cursorWindowClone, null);

        let dragEvent = {
            x,
            y,
            dragActor: this._cursorWindowClone ?? this._dummy,
            source: this,
            targetActor: pickedActor,
        };

        for (let i = 0; i < DND.dragMonitors.length; i++) {
            let motionFunc = DND.dragMonitors[i].dragMotion;
            if (motionFunc) {
                let result = motionFunc(dragEvent);
                if (result !== DND.DragMotionResult.CONTINUE)
                    return;
            }
        }

        while (pickedActor) {
            if (pickedActor._delegate && pickedActor._delegate.handleDragOver) {
                let [r_, targX, targY] = pickedActor.transform_stage_point(x, y);
                let result = pickedActor._delegate.handleDragOver(this,
                    dragEvent.dragActor,
                    targX,
                    targY,
                    global.get_current_time());
                if (result !== DND.DragMotionResult.CONTINUE)
                    return;
            }
            pickedActor = pickedActor.get_parent();
        }
    }
}
(uuay)dash.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as AppDisplay from './appDisplay.js';
import * as AppFavorites from './appFavorites.js';
import * as DND from './dnd.js';
import * as IconGrid from './iconGrid.js';
import * as Main from './main.js';
import * as Overview from './overview.js';

const DASH_ANIMATION_TIME = 200;
const DASH_ITEM_LABEL_SHOW_TIME = 150;
const DASH_ITEM_LABEL_HIDE_TIME = 100;
const DASH_ITEM_HOVER_TIMEOUT = 300;

export const DashIcon = GObject.registerClass(
class DashIcon extends AppDisplay.AppIcon {
    _init(app) {
        super._init(app, {
            setSizeManually: true,
            showLabel: false,
        });
    }

    popupMenu() {
        super.popupMenu(St.Side.BOTTOM);
    }

    // Disable scale-n-fade methods used during DND by parent
    scaleAndFade() {
    }

    undoScaleAndFade() {
    }

    handleDragOver() {
        return DND.DragMotionResult.CONTINUE;
    }

    acceptDrop() {
        return false;
    }
});

// A container like StBin, but taking the child's scale into account
// when requesting a size
export const DashItemContainer = GObject.registerClass(
class DashItemContainer extends St.Widget {
    _init() {
        super._init({
            style_class: 'dash-item-container',
            pivot_point: new Graphene.Point({x: .5, y: .5}),
            layout_manager: new Clutter.BinLayout(),
            scale_x: 0,
            scale_y: 0,
            opacity: 0,
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
        });

        this._labelText = '';
        this.label = new St.Label({style_class: 'dash-label'});
        this.label.hide();
        Main.layoutManager.addChrome(this.label);
        this.label.connectObject('destroy', () => (this.label = null), this);
        this.label_actor = this.label;

        this.child = null;
        this.animatingOut = false;

        this.connect('notify::scale-x', () => this.queue_relayout());
        this.connect('notify::scale-y', () => this.queue_relayout());

        this.connect('destroy', () => {
            if (this.child != null)
                this.child.destroy();
            this.label?.destroy();
        });
    }

    vfunc_get_preferred_height(forWidth) {
        let themeNode = this.get_theme_node();
        forWidth = themeNode.adjust_for_width(forWidth);
        let [minHeight, natHeight] = super.vfunc_get_preferred_height(forWidth);
        return themeNode.adjust_preferred_height(
            minHeight * this.scale_y,
            natHeight * this.scale_y);
    }

    vfunc_get_preferred_width(forHeight) {
        let themeNode = this.get_theme_node();
        forHeight = themeNode.adjust_for_height(forHeight);
        let [minWidth, natWidth] = super.vfunc_get_preferred_width(forHeight);
        return themeNode.adjust_preferred_width(
            minWidth * this.scale_x,
            natWidth * this.scale_x);
    }

    showLabel() {
        if (!this._labelText)
            return;

        this.label.set_text(this._labelText);
        this.label.opacity = 0;
        this.label.show();

        let [stageX, stageY] = this.get_transformed_position();

        const itemWidth = this.allocation.get_width();

        const labelWidth = this.label.get_width();
        const xOffset = Math.floor((itemWidth - labelWidth) / 2);
        const x = Math.clamp(stageX + xOffset, 0, global.stage.width - labelWidth);

        let node = this.label.get_theme_node();
        const yOffset = node.get_length('-y-offset');

        const y = stageY - this.label.height - yOffset;

        this.label.set_position(x, y);
        this.label.ease({
            opacity: 255,
            duration: DASH_ITEM_LABEL_SHOW_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    setLabelText(text) {
        this._labelText = text;
        this.child.accessible_name = text;
    }

    hideLabel() {
        this.label.ease({
            opacity: 0,
            duration: DASH_ITEM_LABEL_HIDE_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this.label.hide(),
        });
    }

    setChild(actor) {
        if (this.child === actor)
            return;

        this.destroy_all_children();

        this.child = actor;
        this.child.y_expand = true;
        this.add_child(this.child);
    }

    show(animate) {
        if (this.child == null)
            return;

        let time = animate ? DASH_ANIMATION_TIME : 0;
        this.ease({
            scale_x: 1,
            scale_y: 1,
            opacity: 255,
            duration: time,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    animateOutAndDestroy() {
        this.label.hide();

        if (this.child == null) {
            this.destroy();
            return;
        }

        this.animatingOut = true;
        this.ease({
            scale_x: 0,
            scale_y: 0,
            opacity: 0,
            duration: DASH_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this.destroy(),
        });
    }
});

export const ShowAppsIcon = GObject.registerClass(
class ShowAppsIcon extends DashItemContainer {
    _init() {
        super._init();

        this.toggleButton = new St.Button({
            style_class: 'show-apps',
            track_hover: true,
            can_focus: true,
            toggle_mode: true,
        });
        this._iconActor = null;
        this.icon = new IconGrid.BaseIcon(_('Show Apps'), {
            setSizeManually: true,
            showLabel: false,
            createIcon: this._createIcon.bind(this),
        });
        this.icon.y_align = Clutter.ActorAlign.CENTER;

        this.toggleButton.child = this.icon;
        this.toggleButton._delegate = this;

        this.setChild(this.toggleButton);
        this.setDragApp(null);
    }

    _createIcon(size) {
        this._iconActor = new St.Icon({
            icon_name: 'view-app-grid-symbolic',
            icon_size: size,
            style_class: 'show-apps-icon',
            track_hover: true,
        });
        return this._iconActor;
    }

    _canRemoveApp(app) {
        if (app == null)
            return false;

        if (!global.settings.is_writable('favorite-apps'))
            return false;

        let id = app.get_id();
        let isFavorite = AppFavorites.getAppFavorites().isFavorite(id);
        return isFavorite;
    }

    setDragApp(app) {
        let canRemove = this._canRemoveApp(app);

        this.toggleButton.set_hover(canRemove);
        if (this._iconActor)
            this._iconActor.set_hover(canRemove);

        if (canRemove)
            this.setLabelText(_('Unpin'));
        else
            this.setLabelText(_('Show Apps'));
    }

    handleDragOver(source, _actor, _x, _y, _time) {
        if (!this._canRemoveApp(Dash.getAppFromSource(source)))
            return DND.DragMotionResult.NO_DROP;

        return DND.DragMotionResult.MOVE_DROP;
    }

    acceptDrop(source, _actor, _x, _y, _time) {
        const app = Dash.getAppFromSource(source);
        if (!this._canRemoveApp(app))
            return false;

        let id = app.get_id();

        const laters = global.compositor.get_laters();
        laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
            AppFavorites.getAppFavorites().removeFavorite(id);
            return false;
        });

        return true;
    }
});

const DragPlaceholderItem = GObject.registerClass(
class DragPlaceholderItem extends DashItemContainer {
    _init() {
        super._init();
        this.setChild(new St.Bin({style_class: 'placeholder'}));
    }
});

const EmptyDropTargetItem = GObject.registerClass(
class EmptyDropTargetItem extends DashItemContainer {
    _init() {
        super._init();
        this.setChild(new St.Bin({style_class: 'empty-dash-drop-target'}));
    }
});

const DashIconsLayout = GObject.registerClass(
class DashIconsLayout extends Clutter.BoxLayout {
    _init() {
        super._init({
            orientation: Clutter.Orientation.HORIZONTAL,
        });
    }

    vfunc_get_preferred_width(container, forHeight) {
        const [, natWidth] = super.vfunc_get_preferred_width(container, forHeight);
        return [0, natWidth];
    }
});

const baseIconSizes = [16, 22, 24, 32, 48, 64];

export const Dash = GObject.registerClass({
    Signals: {'icon-size-changed': {}},
}, class Dash extends St.Widget {
    /**
     * @param {object} source
     */
    static getAppFromSource(source) {
        if (source instanceof AppDisplay.AppIcon)
            return source.app;
        else
            return null;
    }

    _init() {
        this._maxWidth = -1;
        this._maxHeight = -1;
        this.iconSize = 64;
        this._shownInitially = false;

        this._separator = null;
        this._dragPlaceholder = null;
        this._dragPlaceholderPos = -1;
        this._animatingPlaceholdersCount = 0;
        this._showLabelTimeoutId = 0;
        this._resetHoverTimeoutId = 0;
        this._labelShowing = false;

        super._init({
            name: 'dash',
            offscreen_redirect: Clutter.OffscreenRedirect.ALWAYS,
            layout_manager: new Clutter.BinLayout(),
        });

        this._dashContainer = new St.BoxLayout({
            x_align: Clutter.ActorAlign.CENTER,
            y_expand: true,
        });

        this._box = new St.Widget({
            clip_to_allocation: true,
            layout_manager: new DashIconsLayout(),
            y_expand: true,
        });
        this._box._delegate = this;

        this._dashContainer.add_child(this._box);

        this._showAppsIcon = new ShowAppsIcon();
        this._showAppsIcon.show(false);
        this._showAppsIcon.icon.setIconSize(this.iconSize);
        this._hookUpLabel(this._showAppsIcon);
        this._dashContainer.add_child(this._showAppsIcon);

        this.showAppsButton = this._showAppsIcon.toggleButton;

        this._background = new St.Widget({
            style_class: 'dash-background',
        });

        const sizerBox = new Clutter.Actor();
        sizerBox.add_constraint(new Clutter.BindConstraint({
            source: this._showAppsIcon.icon,
            coordinate: Clutter.BindCoordinate.HEIGHT,
        }));
        sizerBox.add_constraint(new Clutter.BindConstraint({
            source: this._dashContainer,
            coordinate: Clutter.BindCoordinate.WIDTH,
        }));
        this._background.add_child(sizerBox);

        this.add_child(this._background);
        this.add_child(this._dashContainer);

        this._workId = Main.initializeDeferredWork(this._box, this._redisplay.bind(this));

        this._appSystem = Shell.AppSystem.get_default();

        this._appSystem.connect('installed-changed', () => {
            AppFavorites.getAppFavorites().reload();
            this._queueRedisplay();
        });
        AppFavorites.getAppFavorites().connect('changed', this._queueRedisplay.bind(this));
        this._appSystem.connect('app-state-changed', this._queueRedisplay.bind(this));

        Main.overview.connect('item-drag-begin',
            this._onItemDragBegin.bind(this));
        Main.overview.connect('item-drag-end',
            this._onItemDragEnd.bind(this));
        Main.overview.connect('item-drag-cancelled',
            this._onItemDragCancelled.bind(this));
        Main.overview.connect('window-drag-begin',
            this._onWindowDragBegin.bind(this));
        Main.overview.connect('window-drag-cancelled',
            this._onWindowDragEnd.bind(this));
        Main.overview.connect('window-drag-end',
            this._onWindowDragEnd.bind(this));

        // Translators: this is the name of the dock/favorites area on
        // the bottom of the overview
        Main.ctrlAltTabManager.addGroup(this, _('Dash'), 'shell-focus-dash-symbolic');
    }

    _onItemDragBegin() {
        this._dragCancelled = false;
        this._dragMonitor = {
            dragMotion: this._onItemDragMotion.bind(this),
        };
        DND.addDragMonitor(this._dragMonitor);

        if (this._box.get_n_children() === 0) {
            this._emptyDropTarget = new EmptyDropTargetItem();
            this._box.insert_child_at_index(this._emptyDropTarget, 0);
            this._emptyDropTarget.show(true);
        }
    }

    _onItemDragCancelled() {
        this._dragCancelled = true;
        this._endItemDrag();
    }

    _onItemDragEnd() {
        if (this._dragCancelled)
            return;

        this._endItemDrag();
    }

    _endItemDrag() {
        this._clearDragPlaceholder();
        this._clearEmptyDropTarget();
        this._showAppsIcon.setDragApp(null);
        DND.removeDragMonitor(this._dragMonitor);
    }

    _onItemDragMotion(dragEvent) {
        const app = Dash.getAppFromSource(dragEvent.source);
        if (app == null)
            return DND.DragMotionResult.CONTINUE;

        let showAppsHovered =
                this._showAppsIcon.contains(dragEvent.targetActor);

        if (!this._box.contains(dragEvent.targetActor) || showAppsHovered)
            this._clearDragPlaceholder();

        if (showAppsHovered)
            this._showAppsIcon.setDragApp(app);
        else
            this._showAppsIcon.setDragApp(null);

        return DND.DragMotionResult.CONTINUE;
    }

    _onWindowDragBegin() {
        this.ease({
            opacity: 128,
            duration: Overview.ANIMATION_TIME / 2,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    _onWindowDragEnd() {
        this.ease({
            opacity: 255,
            duration: Overview.ANIMATION_TIME / 2,
            mode: Clutter.AnimationMode.EASE_IN_QUAD,
        });
    }

    _appIdListToHash(apps) {
        let ids = {};
        for (let i = 0; i < apps.length; i++)
            ids[apps[i].get_id()] = apps[i];
        return ids;
    }

    _queueRedisplay() {
        Main.queueDeferredWork(this._workId);
    }

    _hookUpLabel(item, appIcon) {
        item.child.connect('notify::hover', () => {
            this._syncLabel(item, appIcon);
        });

        item.child.connect('clicked', () => {
            this._labelShowing = false;
            item.hideLabel();
        });

        Main.overview.connectObject('hiding', () => {
            this._labelShowing = false;
            item.hideLabel();
        }, item.child);

        if (appIcon) {
            appIcon.connect('sync-tooltip', () => {
                this._syncLabel(item, appIcon);
            });
        }
    }

    _createAppItem(app) {
        let item = new DashItemContainer();
        let appIcon = new DashIcon(app);

        appIcon.connect('menu-state-changed', (o, opened) => {
            this._itemMenuStateChanged(item, opened);
        });

        item.setChild(appIcon);

        // Override default AppIcon label_actor, now the
        // accessible_name is set at DashItemContainer.setLabelText
        appIcon.label_actor = null;
        item.setLabelText(app.get_name());

        appIcon.icon.setIconSize(this.iconSize);
        this._hookUpLabel(item, appIcon);

        return item;
    }

    _itemMenuStateChanged(item, opened) {
        // When the menu closes, it calls sync_hover, which means
        // that the notify::hover handler does everything we need to.
        if (opened) {
            if (this._showLabelTimeoutId > 0) {
                GLib.source_remove(this._showLabelTimeoutId);
                this._showLabelTimeoutId = 0;
            }

            item.hideLabel();
        }
    }

    _syncLabel(item, appIcon) {
        let shouldShow = appIcon ? appIcon.shouldShowTooltip() : item.child.get_hover();

        if (shouldShow) {
            if (this._showLabelTimeoutId === 0) {
                let timeout = this._labelShowing ? 0 : DASH_ITEM_HOVER_TIMEOUT;
                this._showLabelTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout,
                    () => {
                        this._labelShowing = true;
                        item.showLabel();
                        this._showLabelTimeoutId = 0;
                        return GLib.SOURCE_REMOVE;
                    });
                GLib.Source.set_name_by_id(this._showLabelTimeoutId, '[gnome-shell] item.showLabel');
                if (this._resetHoverTimeoutId > 0) {
                    GLib.source_remove(this._resetHoverTimeoutId);
                    this._resetHoverTimeoutId = 0;
                }
            }
        } else {
            if (this._showLabelTimeoutId > 0)
                GLib.source_remove(this._showLabelTimeoutId);
            this._showLabelTimeoutId = 0;
            item.hideLabel();
            if (this._labelShowing) {
                this._resetHoverTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DASH_ITEM_HOVER_TIMEOUT,
                    () => {
                        this._labelShowing = false;
                        this._resetHoverTimeoutId = 0;
                        return GLib.SOURCE_REMOVE;
                    });
                GLib.Source.set_name_by_id(this._resetHoverTimeoutId, '[gnome-shell] this._labelShowing');
            }
        }
    }

    _adjustIconSize() {
        // For the icon size, we only consider children which are "proper"
        // icons (i.e. ignoring drag placeholders) and which are not
        // animating out (which means they will be destroyed at the end of
        // the animation)
        let iconChildren = this._box.get_children().filter(actor => {
            return actor.child &&
                   actor.child._delegate &&
                   actor.child._delegate.icon &&
                   !actor.animatingOut;
        });

        iconChildren.push(this._showAppsIcon);

        if (this._maxWidth === -1 || this._maxHeight === -1)
            return;

        const themeNode = this.get_theme_node();
        const maxAllocation = new Clutter.ActorBox({
            x1: 0,
            y1: 0,
            x2: this._maxWidth,
            y2: 42, /* whatever */
        });
        let maxContent = themeNode.get_content_box(maxAllocation);
        let availWidth = maxContent.x2 - maxContent.x1;
        let spacing = themeNode.get_length('spacing');

        let firstButton = iconChildren[0].child;
        let firstIcon = firstButton._delegate.icon;

        // Enforce valid spacings during the size request
        firstIcon.icon.ensure_style();
        const [, , iconWidth, iconHeight] = firstIcon.icon.get_preferred_size();
        const [, , buttonWidth, buttonHeight] = firstButton.get_preferred_size();

        // Subtract icon padding and box spacing from the available width
        availWidth -= iconChildren.length * (buttonWidth - iconWidth) +
                       (iconChildren.length - 1) * spacing;

        let availHeight = this._maxHeight;
        availHeight -= this.margin_top + this.margin_bottom;
        availHeight -= this._background.get_theme_node().get_vertical_padding();
        availHeight -= themeNode.get_vertical_padding();
        availHeight -= buttonHeight - iconHeight;

        const maxIconSize = Math.min(availWidth / iconChildren.length, availHeight);

        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        let iconSizes = baseIconSizes.map(s => s * scaleFactor);

        let newIconSize = baseIconSizes[0];
        for (let i = 0; i < iconSizes.length; i++) {
            if (iconSizes[i] <= maxIconSize)
                newIconSize = baseIconSizes[i];
        }

        if (newIconSize === this.iconSize)
            return;

        let oldIconSize = this.iconSize;
        this.iconSize = newIconSize;
        this.emit('icon-size-changed');

        let scale = oldIconSize / newIconSize;
        for (let i = 0; i < iconChildren.length; i++) {
            let icon = iconChildren[i].child._delegate.icon;

            // Set the new size immediately, to keep the icons' sizes
            // in sync with this.iconSize
            icon.setIconSize(this.iconSize);

            // Don't animate the icon size change when the overview
            // is transitioning, not visible or when initially filling
            // the dash
            if (!Main.overview.visible || Main.overview.animationInProgress ||
                !this._shownInitially)
                continue;

            let [targetWidth, targetHeight] = icon.icon.get_size();

            // Scale the icon's texture to the previous size and
            // tween to the new size
            icon.icon.set_size(
                icon.icon.width * scale,
                icon.icon.height * scale);

            icon.icon.ease({
                width: targetWidth,
                height: targetHeight,
                duration: DASH_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        }

        if (this._separator) {
            this._separator.ease({
                height: this.iconSize,
                duration: DASH_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        }
    }

    _redisplay() {
        let favorites = AppFavorites.getAppFavorites().getFavoriteMap();

        let running = this._appSystem.get_running();

        let children = this._box.get_children().filter(actor => {
            return actor.child &&
                   actor.child._delegate &&
                   actor.child._delegate.app;
        });
        // Apps currently in the dash
        let oldApps = children.map(actor => actor.child._delegate.app);
        // Apps supposed to be in the dash
        let newApps = [];

        for (let id in favorites)
            newApps.push(favorites[id]);

        for (let i = 0; i < running.length; i++) {
            let app = running[i];
            if (app.get_id() in favorites)
                continue;
            newApps.push(app);
        }

        // Figure out the actual changes to the list of items; we iterate
        // over both the list of items currently in the dash and the list
        // of items expected there, and collect additions and removals.
        // Moves are both an addition and a removal, where the order of
        // the operations depends on whether we encounter the position
        // where the item has been added first or the one from where it
        // was removed.
        // There is an assumption that only one item is moved at a given
        // time; when moving several items at once, everything will still
        // end up at the right position, but there might be additional
        // additions/removals (e.g. it might remove all the launchers
        // and add them back in the new order even if a smaller set of
        // additions and removals is possible).
        // If above assumptions turns out to be a problem, we might need
        // to use a more sophisticated algorithm, e.g. Longest Common
        // Subsequence as used by diff.
        let addedItems = [];
        let removedActors = [];

        let newIndex = 0;
        let oldIndex = 0;
        while (newIndex < newApps.length || oldIndex < oldApps.length) {
            let oldApp = oldApps.length > oldIndex ? oldApps[oldIndex] : null;
            let newApp = newApps.length > newIndex ? newApps[newIndex] : null;

            // No change at oldIndex/newIndex
            if (oldApp === newApp) {
                oldIndex++;
                newIndex++;
                continue;
            }

            // App removed at oldIndex
            if (oldApp && !newApps.includes(oldApp)) {
                removedActors.push(children[oldIndex]);
                oldIndex++;
                continue;
            }

            // App added at newIndex
            if (newApp && !oldApps.includes(newApp)) {
                addedItems.push({
                    app: newApp,
                    item: this._createAppItem(newApp),
                    pos: newIndex,
                });
                newIndex++;
                continue;
            }

            // App moved
            let nextApp = newApps.length > newIndex + 1
                ? newApps[newIndex + 1] : null;
            let insertHere = nextApp && nextApp === oldApp;
            let alreadyRemoved = removedActors.reduce((result, actor) => {
                let removedApp = actor.child._delegate.app;
                return result || removedApp === newApp;
            }, false);

            if (insertHere || alreadyRemoved) {
                let newItem = this._createAppItem(newApp);
                addedItems.push({
                    app: newApp,
                    item: newItem,
                    pos: newIndex + removedActors.length,
                });
                newIndex++;
            } else {
                removedActors.push(children[oldIndex]);
                oldIndex++;
            }
        }

        for (let i = 0; i < addedItems.length; i++) {
            this._box.insert_child_at_index(
                addedItems[i].item,
                addedItems[i].pos);
        }

        for (let i = 0; i < removedActors.length; i++) {
            let item = removedActors[i];

            // Don't animate item removal when the overview is transitioning
            // or hidden
            if (Main.overview.visible && !Main.overview.animationInProgress)
                item.animateOutAndDestroy();
            else
                item.destroy();
        }

        this._adjustIconSize();

        // Skip animations on first run when adding the initial set
        // of items, to avoid all items zooming in at once

        let animate = this._shownInitially && Main.overview.visible &&
            !Main.overview.animationInProgress;

        if (!this._shownInitially)
            this._shownInitially = true;

        for (let i = 0; i < addedItems.length; i++)
            addedItems[i].item.show(animate);

        // Update separator
        const nFavorites = Object.keys(favorites).length;
        const nIcons = children.length + addedItems.length - removedActors.length;
        if (nFavorites > 0 && nFavorites < nIcons) {
            if (!this._separator) {
                this._separator = new St.Widget({
                    style_class: 'dash-separator',
                    y_align: Clutter.ActorAlign.CENTER,
                    height: this.iconSize,
                });
                this._box.add_child(this._separator);
            }
            let pos = nFavorites + this._animatingPlaceholdersCount;
            if (this._dragPlaceholder)
                pos++;
            this._box.set_child_at_index(this._separator, pos);
        } else if (this._separator) {
            this._separator.destroy();
            this._separator = null;
        }

        // Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=692744
        // Without it, StBoxLayout may use a stale size cache
        this._box.queue_relayout();
    }

    _clearDragPlaceholder() {
        if (this._dragPlaceholder) {
            this._animatingPlaceholdersCount++;
            this._dragPlaceholder.connect('destroy', () => {
                this._animatingPlaceholdersCount--;
            });
            this._dragPlaceholder.animateOutAndDestroy();
            this._dragPlaceholder = null;
        }
        this._dragPlaceholderPos = -1;
    }

    _clearEmptyDropTarget() {
        if (this._emptyDropTarget) {
            this._emptyDropTarget.animateOutAndDestroy();
            this._emptyDropTarget = null;
        }
    }

    handleDragOver(source, actor, x, _y, _time) {
        const app = Dash.getAppFromSource(source);

        // Don't allow favoriting of transient apps
        if (app == null || app.is_window_backed())
            return DND.DragMotionResult.NO_DROP;

        if (!global.settings.is_writable('favorite-apps'))
            return DND.DragMotionResult.NO_DROP;

        let favorites = AppFavorites.getAppFavorites().getFavorites();
        let numFavorites = favorites.length;

        let favPos = favorites.indexOf(app);

        let children = this._box.get_children();
        let numChildren = children.length;
        let boxWidth = this._box.width;

        // Keep the placeholder out of the index calculation; assuming that
        // the remove target has the same size as "normal" items, we don't
        // need to do the same adjustment there.
        if (this._dragPlaceholder) {
            boxWidth -= this._dragPlaceholder.width;
            numChildren--;
        }

        // Same with the separator
        if (this._separator) {
            boxWidth -= this._separator.width;
            numChildren--;
        }

        let pos;
        if (this._emptyDropTarget)
            pos = 0; // always insert at the start when dash is empty
        else if (this.text_direction === Clutter.TextDirection.RTL)
            pos = numChildren - Math.floor(x * numChildren / boxWidth);
        else
            pos = Math.floor(x * numChildren / boxWidth);

        // Put the placeholder after the last favorite if we are not
        // in the favorites zone
        if (pos > numFavorites)
            pos = numFavorites;

        if (pos !== this._dragPlaceholderPos && this._animatingPlaceholdersCount === 0) {
            this._dragPlaceholderPos = pos;

            // Don't allow positioning before or after self
            if (favPos !== -1 && (pos === favPos || pos === favPos + 1)) {
                this._clearDragPlaceholder();
                return DND.DragMotionResult.CONTINUE;
            }

            // If the placeholder already exists, we just move
            // it, but if we are adding it, expand its size in
            // an animation
            let fadeIn;
            if (this._dragPlaceholder) {
                this._dragPlaceholder.destroy();
                fadeIn = false;
            } else {
                fadeIn = true;
            }

            this._dragPlaceholder = new DragPlaceholderItem();
            this._dragPlaceholder.child.set_width(this.iconSize);
            this._dragPlaceholder.child.set_height(this.iconSize / 2);
            this._box.insert_child_at_index(
                this._dragPlaceholder,
                this._dragPlaceholderPos);
            this._dragPlaceholder.show(fadeIn);
        }

        if (!this._dragPlaceholder)
            return DND.DragMotionResult.NO_DROP;

        let srcIsFavorite = favPos !== -1;

        if (srcIsFavorite)
            return DND.DragMotionResult.MOVE_DROP;

        return DND.DragMotionResult.COPY_DROP;
    }

    // Draggable target interface
    acceptDrop(source, _actor, _x, _y, _time) {
        const app = Dash.getAppFromSource(source);

        // Don't allow favoriting of transient apps
        if (app == null || app.is_window_backed())
            return false;

        if (!global.settings.is_writable('favorite-apps'))
            return false;

        let id = app.get_id();

        let favorites = AppFavorites.getAppFavorites().getFavoriteMap();

        let srcIsFavorite = id in favorites;

        let favPos = 0;
        let children = this._box.get_children();
        for (let i = 0; i < this._dragPlaceholderPos; i++) {
            if (this._dragPlaceholder &&
                children[i] === this._dragPlaceholder)
                continue;

            let childId = children[i].child._delegate.app.get_id();
            if (childId === id)
                continue;
            if (childId in favorites)
                favPos++;
        }

        // No drag placeholder means we don't want to favorite the app
        // and we are dragging it to its original position
        if (!this._dragPlaceholder)
            return true;

        const laters = global.compositor.get_laters();
        laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
            let appFavorites = AppFavorites.getAppFavorites();
            if (srcIsFavorite)
                appFavorites.moveFavoriteToPos(id, favPos);
            else
                appFavorites.addFavoriteAtPos(id, favPos);
            return false;
        });

        return true;
    }

    setMaxSize(maxWidth, maxHeight) {
        if (this._maxWidth === maxWidth &&
            this._maxHeight === maxHeight)
            return;

        this._maxWidth = maxWidth;
        this._maxHeight = maxHeight;
        this._queueRedisplay();
    }
});
(uuay)remoteSearch.js�+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GdkPixbuf from 'gi://GdkPixbuf';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as FileUtils from '../misc/fileUtils.js';

const KEY_FILE_GROUP = 'Shell Search Provider';

const SearchProviderIface = `
<node>
<interface name="org.gnome.Shell.SearchProvider">
<method name="GetInitialResultSet">
    <arg type="as" direction="in" />
    <arg type="as" direction="out" />
</method>
<method name="GetSubsearchResultSet">
    <arg type="as" direction="in" />
    <arg type="as" direction="in" />
    <arg type="as" direction="out" />
</method>
<method name="GetResultMetas">
    <arg type="as" direction="in" />
    <arg type="aa{sv}" direction="out" />
</method>
<method name="ActivateResult">
    <arg type="s" direction="in" />
</method>
<method name="XUbuntuCancel" />
</interface>
</node>`;

const SearchProvider2Iface = `
<node>
<interface name="org.gnome.Shell.SearchProvider2">
<method name="GetInitialResultSet">
    <arg type="as" direction="in" />
    <arg type="as" direction="out" />
</method>
<method name="GetSubsearchResultSet">
    <arg type="as" direction="in" />
    <arg type="as" direction="in" />
    <arg type="as" direction="out" />
</method>
<method name="GetResultMetas">
    <arg type="as" direction="in" />
    <arg type="aa{sv}" direction="out" />
</method>
<method name="ActivateResult">
    <arg type="s" direction="in" />
    <arg type="as" direction="in" />
    <arg type="u" direction="in" />
</method>
<method name="LaunchSearch">
    <arg type="as" direction="in" />
    <arg type="u" direction="in" />
</method>
<method name="XUbuntuCancel" />
</interface>
</node>`;

const SearchProviderProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProviderIface);
const SearchProvider2ProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProvider2Iface);

/**
 * loadRemoteSearchProviders:
 *
 * @param {Gio.Settings} searchSettings - search settings
 * @returns {RemoteSearchProvider[]} - the list of remote providers
 */
export function loadRemoteSearchProviders(searchSettings) {
    let objectPaths = {};
    let loadedProviders = [];

    function loadRemoteSearchProvider(file) {
        let keyfile = new GLib.KeyFile();
        let path = file.get_path();

        try {
            keyfile.load_from_file(path, 0);
        } catch (e) {
            return;
        }

        if (!keyfile.has_group(KEY_FILE_GROUP))
            return;

        let remoteProvider;
        try {
            let group = KEY_FILE_GROUP;
            let busName = keyfile.get_string(group, 'BusName');
            let objectPath = keyfile.get_string(group, 'ObjectPath');

            if (objectPaths[objectPath])
                return;

            let appInfo = null;
            try {
                let desktopId = keyfile.get_string(group, 'DesktopId');
                appInfo = Gio.DesktopAppInfo.new(desktopId);
                if (!appInfo.should_show())
                    return;
            } catch (e) {
                log(`Ignoring search provider ${path}: missing DesktopId`);
                return;
            }

            let autoStart = true;
            try {
                autoStart = keyfile.get_boolean(group, 'AutoStart');
            } catch (e) {
                // ignore error
            }

            let version = '1';
            try {
                version = keyfile.get_string(group, 'Version');
            } catch (e) {
                // ignore error
            }

            if (version >= 2)
                remoteProvider = new RemoteSearchProvider2(appInfo, busName, objectPath, autoStart);
            else
                remoteProvider = new RemoteSearchProvider(appInfo, busName, objectPath, autoStart);

            remoteProvider.defaultEnabled = true;
            try {
                remoteProvider.defaultEnabled = !keyfile.get_boolean(group, 'DefaultDisabled');
            } catch (e) {
                // ignore error
            }

            objectPaths[objectPath] = remoteProvider;
            loadedProviders.push(remoteProvider);
        } catch (e) {
            log(`Failed to add search provider ${path}: ${e}`);
        }
    }

    if (searchSettings.get_boolean('disable-external'))
        return [];

    for (const {dir} of FileUtils.collectFromDatadirs('search-providers', false))
        loadRemoteSearchProvider(dir);

    let sortOrder = searchSettings.get_strv('sort-order');

    const disabled = searchSettings.get_strv('disabled');
    const enabled = searchSettings.get_strv('enabled');

    loadedProviders = loadedProviders.filter(provider => {
        let appId = provider.appInfo.get_id();

        if (provider.defaultEnabled)
            return !disabled.includes(appId);
        else
            return enabled.includes(appId);
    });

    loadedProviders.sort((providerA, providerB) => {
        let idxA, idxB;
        let appIdA, appIdB;

        appIdA = providerA.appInfo.get_id();
        appIdB = providerB.appInfo.get_id();

        idxA = sortOrder.indexOf(appIdA);
        idxB = sortOrder.indexOf(appIdB);

        // if no provider is found in the order, use alphabetical order
        if ((idxA === -1) && (idxB === -1)) {
            let nameA = providerA.appInfo.get_name();
            let nameB = providerB.appInfo.get_name();

            return GLib.utf8_collate(nameA, nameB);
        }

        // if providerA isn't found, it's sorted after providerB
        if (idxA === -1)
            return 1;

        // if providerB isn't found, it's sorted after providerA
        if (idxB === -1)
            return -1;

        // finally, if both providers are found, return their order in the list
        return idxA - idxB;
    });

    return loadedProviders;
}

class RemoteSearchProvider {
    constructor(appInfo, dbusName, dbusPath, autoStart, proxyInfo) {
        if (!proxyInfo)
            proxyInfo = SearchProviderProxyInfo;

        let gFlags = Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES;
        if (autoStart)
            gFlags |= Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION;
        else
            gFlags |= Gio.DBusProxyFlags.DO_NOT_AUTO_START;

        this.proxy = new Gio.DBusProxy({
            g_bus_type: Gio.BusType.SESSION,
            g_name: dbusName,
            g_object_path: dbusPath,
            g_interface_info: proxyInfo,
            g_interface_name: proxyInfo.name,
            gFlags,
        });
        this.proxy.init_async(GLib.PRIORITY_DEFAULT, null);

        this.appInfo = appInfo;
        this.id = appInfo.get_id();
        this.isRemoteProvider = true;
        this.canLaunchSearch = false;
    }

    createIcon(size, meta) {
        let gicon = null;
        let icon = null;

        if (meta['icon']) {
            gicon = Gio.icon_deserialize(meta['icon']);
        } else if (meta['gicon']) {
            gicon = Gio.icon_new_for_string(meta['gicon']);
        } else if (meta['icon-data']) {
            const [
                width, height, rowStride, hasAlpha,
                bitsPerSample, nChannels_, data,
            ] = meta['icon-data'];
            gicon = Shell.util_create_pixbuf_from_data(data,
                GdkPixbuf.Colorspace.RGB,
                hasAlpha,
                bitsPerSample,
                width,
                height,
                rowStride);
        }

        if (gicon)
            icon = new St.Icon({gicon, icon_size: size});
        return icon;
    }

    filterResults(results, maxNumber) {
        if (results.length <= maxNumber)
            return results;

        let regularResults = results.filter(r => !r.startsWith('special:'));
        let specialResults = results.filter(r => r.startsWith('special:'));

        return regularResults.slice(0, maxNumber).concat(specialResults.slice(0, maxNumber));
    }

    async getInitialResultSet(terms, cancellable) {
        try {
            const [results] = await this.proxy.GetInitialResultSetAsync(terms, cancellable);
            return results;
        } catch (error) {
            if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                log(`Received error from D-Bus search provider ${this.id}: ${error}`);
            return [];
        }
    }

    async getSubsearchResultSet(previousResults, newTerms, cancellable) {
        try {
            const [results] = await this.proxy.GetSubsearchResultSetAsync(previousResults, newTerms, cancellable);
            return results;
        } catch (error) {
            if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                log(`Received error from D-Bus search provider ${this.id}: ${error}`);
            return [];
        }
    }

    async getResultMetas(ids, cancellable) {
        let metas;
        try {
            [metas] = await this.proxy.GetResultMetasAsync(ids, cancellable);
        } catch (error) {
            if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                log(`Received error from D-Bus search provider ${this.id} during GetResultMetas: ${error}`);
            return [];
        }

        let resultMetas = [];
        for (let i = 0; i < metas.length; i++) {
            for (let prop in metas[i]) {
                // we can use the serialized icon variant directly
                if (prop !== 'icon')
                    metas[i][prop] = metas[i][prop].deepUnpack();
            }

            resultMetas.push({
                id: metas[i]['id'],
                name: metas[i]['name'],
                description: metas[i]['description'],
                createIcon: size => this.createIcon(size, metas[i]),
                clipboardText: metas[i]['clipboardText'],
            });
        }
        return resultMetas;
    }

    async XUbuntuCancel(cancellable) {
        try {
            await this.proxy.XUbuntuCancelAsync(cancellable);
        } catch (error) {
            if (!error.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD) &&
                !error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                log(`Received error from D-Bus search provider ${this.id} during XUbuntuCancel: ${error}`);
        }
    }

    activateResult(id) {
        this.proxy.ActivateResultAsync(id).catch(logError);
    }

    launchSearch(_terms) {
        // the provider is not compatible with the new version of the interface, launch
        // the app itself but warn so we can catch the error in logs
        log(`Search provider ${this.appInfo.get_id()} does not implement LaunchSearch`);
        this.appInfo.launch([], global.create_app_launch_context(0, -1));
    }
}

class RemoteSearchProvider2 extends RemoteSearchProvider {
    constructor(appInfo, dbusName, dbusPath, autoStart) {
        super(appInfo, dbusName, dbusPath, autoStart, SearchProvider2ProxyInfo);

        this.canLaunchSearch = true;
    }

    activateResult(id, terms) {
        this.proxy.ActivateResultAsync(
            id, terms, global.get_current_time()).catch(logError);
    }

    launchSearch(terms) {
        this.proxy.LaunchSearchAsync(
            terms, global.get_current_time()).catch(logError);
    }
}
(uuay)dialog.js((// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import St from 'gi://St';

function _setLabel(label, value) {
    label.set({
        text: value || '',
        visible: value !== null,
    });
}

export const Dialog = GObject.registerClass(
class Dialog extends St.Widget {
    _init(parentActor, styleClass) {
        super._init({
            layout_manager: new Clutter.BinLayout(),
            reactive: true,
        });
        this.connect('destroy', this._onDestroy.bind(this));

        this._initialKeyFocus = null;
        this._pressedKey = null;
        this._buttonKeys = {};
        this._createDialog();
        this.add_child(this._dialog);

        if (styleClass != null)
            this._dialog.add_style_class_name(styleClass);

        this._parentActor = parentActor;
        this._parentActor.add_child(this);
    }

    _createDialog() {
        this._dialog = new St.BoxLayout({
            style_class: 'modal-dialog',
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
            vertical: true,
        });

        // modal dialogs are fixed width and grow vertically; set the request
        // mode accordingly so wrapped labels are handled correctly during
        // size requests.
        this._dialog.request_mode = Clutter.RequestMode.HEIGHT_FOR_WIDTH;
        this._dialog.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);

        this.contentLayout = new St.BoxLayout({
            vertical: true,
            style_class: 'modal-dialog-content-box',
            y_expand: true,
        });
        this._dialog.add_child(this.contentLayout);

        this.buttonLayout = new St.Widget({
            layout_manager: new Clutter.BoxLayout({homogeneous: true}),
        });
        this._dialog.add_child(this.buttonLayout);
    }

    makeInactive() {
        this.buttonLayout.get_children().forEach(c => c.set_reactive(false));
    }

    _onDestroy() {
        this.makeInactive();
    }

    vfunc_event(event) {
        if (event.type() === Clutter.EventType.KEY_PRESS) {
            this._pressedKey = event.get_key_symbol();
        } else if (event.type() === Clutter.EventType.KEY_RELEASE) {
            let pressedKey = this._pressedKey;
            this._pressedKey = null;

            let symbol = event.get_key_symbol();
            if (symbol !== pressedKey)
                return Clutter.EVENT_PROPAGATE;

            let buttonInfo = this._buttonKeys[symbol];
            if (!buttonInfo)
                return Clutter.EVENT_PROPAGATE;

            let {button, action} = buttonInfo;

            if (action && button.reactive) {
                action();
                return Clutter.EVENT_STOP;
            }
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _setInitialKeyFocus(actor) {
        this._initialKeyFocus?.disconnectObject(this);

        this._initialKeyFocus = actor;

        actor.connectObject('destroy',
            () => (this._initialKeyFocus = null), this);
    }

    get initialKeyFocus() {
        return this._initialKeyFocus || this;
    }

    addButton(buttonInfo) {
        let {label, action, key} = buttonInfo;
        let isDefault = buttonInfo['default'];
        let keys;

        if (key)
            keys = [key];
        else if (isDefault)
            keys = [Clutter.KEY_Return, Clutter.KEY_KP_Enter, Clutter.KEY_ISO_Enter];
        else
            keys = [];

        let button = new St.Button({
            style_class: 'modal-dialog-linked-button',
            button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
            reactive: true,
            can_focus: true,
            x_expand: true,
            y_expand: true,
            label,
        });
        button.connect('clicked', () => action());

        buttonInfo['button'] = button;

        if (isDefault)
            button.add_style_pseudo_class('default');

        if (this._initialKeyFocus == null || isDefault)
            this._setInitialKeyFocus(button);

        for (let i in keys)
            this._buttonKeys[keys[i]] = buttonInfo;

        this.buttonLayout.add_child(button);

        return button;
    }

    clearButtons() {
        this.buttonLayout.destroy_all_children();
        this._buttonKeys = {};
    }
});

export const MessageDialogContent = GObject.registerClass({
    Properties: {
        'title': GObject.ParamSpec.string(
            'title', 'title', 'title',
            GObject.ParamFlags.READWRITE |
            GObject.ParamFlags.CONSTRUCT,
            null),
        'description': GObject.ParamSpec.string(
            'description', 'description', 'description',
            GObject.ParamFlags.READWRITE |
            GObject.ParamFlags.CONSTRUCT,
            null),
    },
}, class MessageDialogContent extends St.BoxLayout {
    _init(params) {
        this._title = new St.Label({style_class: 'message-dialog-title'});
        this._description = new St.Label({style_class: 'message-dialog-description'});

        this._description.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this._description.clutter_text.line_wrap = true;

        super._init({
            style_class: 'message-dialog-content',
            x_expand: true,
            vertical: true,
            ...params,
        });

        this.connect('notify::size', this._updateTitleStyle.bind(this));
        this.connect('destroy', this._onDestroy.bind(this));

        this.add_child(this._title);
        this.add_child(this._description);
    }

    _onDestroy() {
        if (this._updateTitleStyleLater) {
            const laters = global.compositor.get_laters();
            laters.remove(this._updateTitleStyleLater);
            delete this._updateTitleStyleLater;
        }
    }

    get title() {
        return this._title.text;
    }

    get description() {
        return this._description.text;
    }

    _updateTitleStyle() {
        if (!this._title.mapped)
            return;

        this._title.ensure_style();
        const [, titleNatWidth] = this._title.get_preferred_width(-1);

        if (titleNatWidth > this.width) {
            if (this._updateTitleStyleLater)
                return;

            const laters = global.compositor.get_laters();
            this._updateTitleStyleLater = laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                this._updateTitleStyleLater = 0;
                this._title.add_style_class_name('lightweight');
                return GLib.SOURCE_REMOVE;
            });
        }
    }

    set title(title) {
        if (this._title.text === title)
            return;

        _setLabel(this._title, title);

        this._title.remove_style_class_name('lightweight');
        this._updateTitleStyle();

        this.notify('title');
    }

    set description(description) {
        if (this._description.text === description)
            return;

        _setLabel(this._description, description);
        this.notify('description');
    }
});

export const ListSection = GObject.registerClass({
    Properties: {
        'title': GObject.ParamSpec.string(
            'title', 'title', 'title',
            GObject.ParamFlags.READWRITE |
            GObject.ParamFlags.CONSTRUCT,
            null),
    },
}, class ListSection extends St.BoxLayout {
    _init(params) {
        this._title = new St.Label({style_class: 'dialog-list-title'});

        this.list = new St.BoxLayout({
            style_class: 'dialog-list-box',
            vertical: true,
        });

        this._listScrollView = new St.ScrollView({
            style_class: 'dialog-list-scrollview',
            child: this.list,
        });

        super._init({
            style_class: 'dialog-list',
            x_expand: true,
            vertical: true,
            ...params,
        });

        this.label_actor = this._title;
        this.add_child(this._title);
        this.add_child(this._listScrollView);
    }

    get title() {
        return this._title.text;
    }

    set title(title) {
        _setLabel(this._title, title);
        this.notify('title');
    }
});

export const ListSectionItem = GObject.registerClass({
    Properties: {
        'icon-actor':  GObject.ParamSpec.object(
            'icon-actor', 'icon-actor', 'Icon actor',
            GObject.ParamFlags.READWRITE,
            Clutter.Actor.$gtype),
        'title': GObject.ParamSpec.string(
            'title', 'title', 'title',
            GObject.ParamFlags.READWRITE |
            GObject.ParamFlags.CONSTRUCT,
            null),
        'description': GObject.ParamSpec.string(
            'description', 'description', 'description',
            GObject.ParamFlags.READWRITE |
            GObject.ParamFlags.CONSTRUCT,
            null),
    },
}, class ListSectionItem extends St.BoxLayout {
    _init(params) {
        this._iconActorBin = new St.Bin();

        let textLayout = new St.BoxLayout({
            vertical: true,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });

        this._title = new St.Label({style_class: 'dialog-list-item-title'});

        this._description = new St.Label({
            style_class: 'dialog-list-item-title-description',
        });

        textLayout.add_child(this._title);
        textLayout.add_child(this._description);

        super._init({
            style_class: 'dialog-list-item',
            ...params,
        });

        this.label_actor = this._title;
        this.add_child(this._iconActorBin);
        this.add_child(textLayout);
    }

    get iconActor() {
        return this._iconActorBin.get_child();
    }

    set iconActor(actor) {
        this._iconActorBin.set_child(actor);
        this.notify('icon-actor');
    }

    get title() {
        return this._title.text;
    }

    set title(title) {
        _setLabel(this._title, title);
        this.notify('title');
    }

    get description() {
        return this._description.text;
    }

    set description(description) {
        _setLabel(this._description, description);
        this.notify('description');
    }
});
(uuay)padOsd.js�~// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GDesktopEnums from 'gi://GDesktopEnums';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import Rsvg from 'gi://Rsvg';
import St from 'gi://St';

import * as Signals from '../misc/signals.js';

import * as Main from './main.js';
import * as PopupMenu from './popupMenu.js';
import * as Layout from './layout.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';

const ACTIVE_COLOR = '#729fcf';

const LTR = 0;
const RTL = 1;

const PadChooser = GObject.registerClass({
    Signals: {'pad-selected': {param_types: [Clutter.InputDevice.$gtype]}},
}, class PadChooser extends St.Button {
    _init(device, groupDevices) {
        super._init({
            style_class: 'pad-chooser-button',
            toggle_mode: true,
        });
        this.currentDevice = device;
        this._padChooserMenu = null;

        let arrow = new St.Icon({
            style_class: 'popup-menu-arrow',
            icon_name: 'pan-down-symbolic',
            accessible_role: Atk.Role.ARROW,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.set_child(arrow);
        this._ensureMenu(groupDevices);

        this.connect('destroy', this._onDestroy.bind(this));
    }

    vfunc_clicked() {
        if (this.get_checked()) {
            if (this._padChooserMenu != null)
                this._padChooserMenu.open(true);
            else
                this.set_checked(false);
        } else {
            this._padChooserMenu.close(true);
        }
    }

    _ensureMenu(devices) {
        this._padChooserMenu =  new PopupMenu.PopupMenu(this, 0.5, St.Side.TOP);
        this._padChooserMenu.connect('menu-closed', () => {
            this.set_checked(false);
        });
        this._padChooserMenu.actor.hide();
        Main.uiGroup.add_child(this._padChooserMenu.actor);

        this._menuManager = new PopupMenu.PopupMenuManager(this);
        this._menuManager.addMenu(this._padChooserMenu);

        for (let i = 0; i < devices.length; i++) {
            let device = devices[i];
            if (device === this.currentDevice)
                continue;

            this._padChooserMenu.addAction(device.get_device_name(), () => {
                this.emit('pad-selected', device);
            });
        }
    }

    _onDestroy() {
        this._padChooserMenu.destroy();
    }

    update(devices) {
        if (this._padChooserMenu)
            this._padChooserMenu.actor.destroy();
        this.set_checked(false);
        this._ensureMenu(devices);
    }
});

const KeybindingEntry = GObject.registerClass({
    Signals: {'keybinding-edited': {param_types: [GObject.TYPE_STRING]}},
}, class KeybindingEntry extends St.Entry {
    _init() {
        super._init({hint_text: _('New shortcut…'), style: 'width: 10em'});
    }

    vfunc_captured_event(event) {
        if (event.type() !== Clutter.EventType.KEY_PRESS)
            return Clutter.EVENT_PROPAGATE;

        const str = Meta.accelerator_name(
            event.get_state(), event.get_key_symbol());

        this.set_text(str);
        this.emit('keybinding-edited', str);
        return Clutter.EVENT_STOP;
    }
});

const ActionComboBox = GObject.registerClass({
    Signals: {'action-selected': {param_types: [GObject.TYPE_INT]}},
}, class ActionComboBox extends St.Button {
    _init() {
        super._init({style_class: 'button'});
        this.set_toggle_mode(true);

        const boxLayout = new Clutter.BoxLayout({
            orientation: Clutter.Orientation.HORIZONTAL,
            spacing: 6,
        });
        let box = new St.Widget({layout_manager: boxLayout});
        this.set_child(box);

        this._label = new St.Label({style_class: 'combo-box-label'});
        box.add_child(this._label);

        const arrow = new St.Icon({
            style_class: 'popup-menu-arrow',
            icon_name: 'pan-down-symbolic',
            accessible_role: Atk.Role.ARROW,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(arrow);

        this._editMenu = new PopupMenu.PopupMenu(this, 0, St.Side.TOP);
        this._editMenu.connect('menu-closed', () => {
            this.set_checked(false);
        });
        this._editMenu.actor.hide();
        Main.uiGroup.add_child(this._editMenu.actor);

        this._editMenuManager = new PopupMenu.PopupMenuManager(this);
        this._editMenuManager.addMenu(this._editMenu);

        this._actionLabels = new Map();
        this._actionLabels.set(GDesktopEnums.PadButtonAction.NONE, _('App defined'));
        this._actionLabels.set(GDesktopEnums.PadButtonAction.HELP, _('Show on-screen help'));
        this._actionLabels.set(GDesktopEnums.PadButtonAction.SWITCH_MONITOR, _('Switch monitor'));
        this._actionLabels.set(GDesktopEnums.PadButtonAction.KEYBINDING, _('Assign keystroke'));

        this._buttonItems = [];

        for (let [action, label] of this._actionLabels.entries()) {
            let selectedAction = action;
            let item = this._editMenu.addAction(label, () => {
                this._onActionSelected(selectedAction);
            });

            /* These actions only apply to pad buttons */
            if (selectedAction === GDesktopEnums.PadButtonAction.HELP ||
                selectedAction === GDesktopEnums.PadButtonAction.SWITCH_MONITOR)
                this._buttonItems.push(item);
        }

        this.setAction(GDesktopEnums.PadButtonAction.NONE);
    }

    _onActionSelected(action) {
        this.setAction(action);
        this.popdown();
        this.emit('action-selected', action);
    }

    setAction(action) {
        this._label.set_text(this._actionLabels.get(action));
    }

    popup() {
        this._editMenu.open(true);
    }

    popdown() {
        this._editMenu.close(true);
    }

    vfunc_clicked() {
        if (this.get_checked())
            this.popup();
        else
            this.popdown();
    }

    setButtonActionsActive(active) {
        this._buttonItems.forEach(item => item.setSensitive(active));
    }
});

const ActionEditor = GObject.registerClass({
    Signals: {'done': {}},
}, class ActionEditor extends St.Widget {
    _init() {
        const boxLayout = new Clutter.BoxLayout({
            orientation: Clutter.Orientation.HORIZONTAL,
            spacing: 12,
        });

        super._init({layout_manager: boxLayout});

        this._actionComboBox = new ActionComboBox();
        this._actionComboBox.connect('action-selected', this._onActionSelected.bind(this));
        this.add_child(this._actionComboBox);

        this._keybindingEdit = new KeybindingEntry();
        this._keybindingEdit.connect('keybinding-edited', this._onKeybindingEdited.bind(this));
        this.add_child(this._keybindingEdit);

        this._doneButton = new St.Button({
            label: _('Done'),
            style_class: 'button',
            x_expand: false,
        });
        this._doneButton.connect('clicked', this._onEditingDone.bind(this));
        this.add_child(this._doneButton);
    }

    _updateKeybindingEntryState() {
        if (this._currentAction === GDesktopEnums.PadButtonAction.KEYBINDING) {
            this._keybindingEdit.set_text(this._currentKeybinding);
            this._keybindingEdit.show();
            this._keybindingEdit.grab_key_focus();
        } else {
            this._keybindingEdit.hide();
        }
    }

    setSettings(settings, action) {
        this._buttonSettings = settings;

        this._currentAction = this._buttonSettings.get_enum('action');
        this._currentKeybinding = this._buttonSettings.get_string('keybinding');
        this._actionComboBox.setAction(this._currentAction);
        this._updateKeybindingEntryState();

        let isButton = action === null;
        this._actionComboBox.setButtonActionsActive(isButton);
    }

    close() {
        this._actionComboBox.popdown();
        this.hide();
    }

    _onKeybindingEdited(entry, keybinding) {
        this._currentKeybinding = keybinding;
    }

    _onActionSelected(menu, action) {
        this._currentAction = action;
        this._updateKeybindingEntryState();
    }

    _storeSettings() {
        if (!this._buttonSettings)
            return;

        let keybinding = null;

        if (this._currentAction === GDesktopEnums.PadButtonAction.KEYBINDING)
            keybinding = this._currentKeybinding;

        this._buttonSettings.set_enum('action', this._currentAction);

        if (keybinding)
            this._buttonSettings.set_string('keybinding', keybinding);
        else
            this._buttonSettings.reset('keybinding');
    }

    _onEditingDone() {
        this._storeSettings();
        this.close();
        this.emit('done');
    }
});

const PadDiagram = GObject.registerClass({
    Properties: {
        'left-handed': GObject.ParamSpec.boolean(
            'left-handed', 'left-handed', 'Left handed',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            false),
        'image': GObject.ParamSpec.string(
            'image', 'image', 'Image',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            null),
        'editor-actor': GObject.ParamSpec.object(
            'editor-actor', 'editor-actor', 'Editor actor',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            Clutter.Actor.$gtype),
    },
}, class PadDiagram extends St.DrawingArea {
    _init(params) {
        let file = Gio.File.new_for_uri('resource:///org/gnome/shell/theme/pad-osd.css');
        let [success_, css] = file.load_contents(null);
        this._curEdited = null;
        this._css = new TextDecoder().decode(css);
        this._labels = [];
        this._activeButtons = [];
        super._init(params);
    }

    get image() {
        return this._imagePath;
    }

    set image(imagePath) {
        let originalHandle = Rsvg.Handle.new_from_file(imagePath);
        let dimensions = originalHandle.get_dimensions();
        this._imageWidth = dimensions.width;
        this._imageHeight = dimensions.height;

        this._imagePath = imagePath;
        this._handle = this._composeStyledDiagram();
        this._initLabels();
    }

    get editorActor() {
        return this._editorActor;
    }

    set editorActor(actor) {
        actor.hide();
        this._editorActor = actor;
        this.add_child(actor);
    }

    _initLabels() {
        let i = 0;
        for (i = 0; ; i++) {
            if (!this._addLabel(null, i))
                break;
        }

        for (i = 0; ; i++) {
            if (!this._addLabel(Meta.PadFeatureType.RING, i, Meta.PadDirection.CW) ||
                !this._addLabel(Meta.PadFeatureType.RING, i, Meta.PadDirection.CCW))
                break;
        }

        for (i = 0; ; i++) {
            if (!this._addLabel(Meta.PadFeatureType.STRIP, i, Meta.PadDirection.UP) ||
                !this._addLabel(Meta.PadFeatureType.STRIP, i, Meta.PadDirection.DOWN))
                break;
        }
    }

    _wrappingSvgHeader() {
        return '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' +
               '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" ' +
               'xmlns:xi="http://www.w3.org/2001/XInclude" ' +
               `width="${this._imageWidth}" height="${this._imageHeight}"> ` +
               '<style type="text/css">';
    }

    _wrappingSvgFooter() {
        return '%s%s%s'.format(
            '</style>',
            '<xi:include href="%s" />'.format(this._imagePath),
            '</svg>');
    }

    _cssString() {
        let css = this._css;

        for (let i = 0; i < this._activeButtons.length; i++) {
            let ch = String.fromCharCode('A'.charCodeAt() + this._activeButtons[i]);
            css += `.${ch}.Leader { stroke: ${ACTIVE_COLOR} !important; }`;
            css += `.${ch}.Button { stroke: ${ACTIVE_COLOR} !important; fill: ${ACTIVE_COLOR} !important; }`;
        }

        return css;
    }

    _composeStyledDiagram() {
        let svgData = '';

        if (!GLib.file_test(this._imagePath, GLib.FileTest.EXISTS))
            return null;

        svgData += this._wrappingSvgHeader();
        svgData += this._cssString();
        svgData += this._wrappingSvgFooter();

        let istream = new Gio.MemoryInputStream();
        istream.add_bytes(new GLib.Bytes(svgData));

        return Rsvg.Handle.new_from_stream_sync(istream,
            Gio.File.new_for_path(this._imagePath), 0, null);
    }

    _updateDiagramScale() {
        [this._actorWidth, this._actorHeight] = this.get_size();
        let dimensions = this._handle.get_dimensions();
        let scaleX = this._actorWidth / dimensions.width;
        let scaleY = this._actorHeight / dimensions.height;
        this._scale = Math.min(scaleX, scaleY);
    }

    _allocateChild(child, x, y, direction) {
        let [, natHeight] = child.get_preferred_height(-1);
        let [, natWidth] = child.get_preferred_width(natHeight);
        let childBox = new Clutter.ActorBox();

        // I miss Cairo.Matrix
        let dimensions = this._handle.get_dimensions();
        x = x * this._scale + this._actorWidth / 2 - dimensions.width / 2 * this._scale;
        y = y * this._scale + this._actorHeight / 2 - dimensions.height / 2 * this._scale;

        if (direction === LTR) {
            childBox.x1 = x;
            childBox.x2 = x + natWidth;
        } else {
            childBox.x1 = x - natWidth;
            childBox.x2 = x;
        }

        childBox.y1 = y - natHeight / 2;
        childBox.y2 = y + natHeight / 2;
        child.allocate(childBox);
    }

    vfunc_allocate(box) {
        super.vfunc_allocate(box);
        if (this._handle === null)
            return;

        this._updateDiagramScale();

        for (let i = 0; i < this._labels.length; i++) {
            const {label, x, y, arrangement} = this._labels[i];
            this._allocateChild(label, x, y, arrangement);
        }

        if (this._editorActor && this._curEdited) {
            const {x, y, arrangement} = this._curEdited;
            this._allocateChild(this._editorActor, x, y, arrangement);
        }
    }

    vfunc_repaint() {
        if (this._handle == null)
            return;

        if (this._scale == null)
            this._updateDiagramScale();

        let [width, height] = this.get_surface_size();
        let dimensions = this._handle.get_dimensions();
        let cr = this.get_context();

        cr.save();
        cr.translate(width / 2, height / 2);
        cr.scale(this._scale, this._scale);
        if (this.leftHanded)
            cr.rotate(Math.PI);
        cr.translate(-dimensions.width / 2, -dimensions.height / 2);
        this._handle.render_cairo(cr);
        cr.restore();
        cr.$dispose();
    }

    _getItemLabelCoords(labelName, leaderName) {
        if (this._handle == null)
            return [false];

        const [labelFound, labelPos] = this._handle.get_position_sub(`#${labelName}`);
        const [, labelSize] = this._handle.get_dimensions_sub(`#${labelName}`);
        if (!labelFound)
            return [false];

        const [leaderFound, leaderPos] = this._handle.get_position_sub(`#${leaderName}`);
        const [, leaderSize] = this._handle.get_dimensions_sub(`#${leaderName}`);
        if (!leaderFound)
            return [false];

        let direction;
        if (labelPos.x > leaderPos.x + leaderSize.width)
            direction = LTR;
        else
            direction = RTL;

        let pos = {x: labelPos.x, y: labelPos.y + labelSize.height};
        if (this.leftHanded) {
            direction = 1 - direction;
            pos.x = this._imageWidth - pos.x;
            pos.y = this._imageHeight - pos.y;
        }

        return [true, pos.x, pos.y, direction];
    }

    _getButtonLabels(button) {
        let ch = String.fromCharCode('A'.charCodeAt() + button);
        const labelName = `Label${ch}`;
        const leaderName = `Leader${ch}`;
        return [labelName, leaderName];
    }

    _getRingLabels(number, dir) {
        let numStr = number > 0 ? (number + 1).toString() : '';
        let dirStr = dir === Meta.PadDirection.CW ? 'CW' : 'CCW';
        const labelName = `LabelRing${numStr}${dirStr}`;
        const leaderName = `LeaderRing${numStr}${dirStr}`;
        return [labelName, leaderName];
    }

    _getStripLabels(number, dir) {
        let numStr = number > 0 ? (number + 1).toString() : '';
        let dirStr = dir === Meta.PadDirection.UP ? 'Up' : 'Down';
        const labelName = `LabelStrip${numStr}${dirStr}`;
        const leaderName = `LeaderStrip${numStr}${dirStr}`;
        return [labelName, leaderName];
    }

    _getLabelCoords(action, idx, dir) {
        if (action === Meta.PadFeatureType.RING)
            return this._getItemLabelCoords(...this._getRingLabels(idx, dir));
        else if (action === Meta.PadFeatureType.STRIP)
            return this._getItemLabelCoords(...this._getStripLabels(idx, dir));
        else
            return this._getItemLabelCoords(...this._getButtonLabels(idx));
    }

    _invalidateSvg() {
        if (this._handle == null)
            return;
        this._handle = this._composeStyledDiagram();
        this.queue_repaint();
    }

    activateButton(button) {
        this._activeButtons.push(button);
        this._invalidateSvg();
    }

    deactivateButton(button) {
        for (let i = 0; i < this._activeButtons.length; i++) {
            if (this._activeButtons[i] === button)
                this._activeButtons.splice(i, 1);
        }
        this._invalidateSvg();
    }

    _addLabel(action, idx, dir) {
        let [found, x, y, arrangement] = this._getLabelCoords(action, idx, dir);
        if (!found)
            return false;

        let label = new St.Label();
        this._labels.push({label, action, idx, dir, x, y, arrangement});
        this.add_child(label);
        return true;
    }

    updateLabels(getText) {
        for (let i = 0; i < this._labels.length; i++) {
            const {label, action, idx, dir} = this._labels[i];
            let str = getText(action, idx, dir);
            label.set_text(str);
        }

        this.queue_relayout();
    }

    _applyLabel(label, action, idx, dir, str) {
        if (str !== null)
            label.set_text(str);
        label.show();
    }

    stopEdition(str) {
        this._editorActor.hide();

        if (this._curEdited) {
            const {label, action, idx, dir} = this._curEdited;
            this._applyLabel(label, action, idx, dir, str);
            this._curEdited = null;
        }

        this.queue_relayout();
    }

    startEdition(action, idx, dir) {
        let editedLabel;

        if (this._curEdited)
            return;

        for (let i = 0; i < this._labels.length; i++) {
            if (action === this._labels[i].action &&
                idx === this._labels[i].idx && dir === this._labels[i].dir) {
                this._curEdited = this._labels[i];
                editedLabel = this._curEdited.label;
                break;
            }
        }

        if (this._curEdited == null)
            return;
        this._editorActor.show();
        editedLabel.hide();
        this.queue_relayout();
    }
});

export const PadOsd = GObject.registerClass({
    Signals: {
        'pad-selected': {param_types: [Clutter.InputDevice.$gtype]},
        'closed': {},
    },
}, class PadOsd extends St.BoxLayout {
    _init(padDevice, settings, imagePath, editionMode, monitorIndex) {
        super._init({
            style_class: 'pad-osd-window',
            vertical: true,
            x_expand: true,
            y_expand: true,
            reactive: true,
        });

        this.padDevice = padDevice;
        this._groupPads = [padDevice];
        this._settings = settings;
        this._imagePath = imagePath;
        this._editionMode = editionMode;
        this._padChooser = null;

        let seat = Clutter.get_default_backend().get_default_seat();
        seat.connectObject(
            'device-added', (_seat, device) => {
                if (device.get_device_type() === Clutter.InputDeviceType.PAD_DEVICE &&
                    this.padDevice.is_grouped(device)) {
                    this._groupPads.push(device);
                    this._updatePadChooser();
                }
            },
            'device-removed', (_seat, device) => {
                // If the device is being removed, destroy the padOsd.
                if (device === this.padDevice) {
                    this.destroy();
                } else if (this._groupPads.includes(device)) {
                    // Or update the pad chooser if the device belongs to
                    // the same group.
                    this._groupPads.splice(this._groupPads.indexOf(device), 1);
                    this._updatePadChooser();
                }
            }, this);

        seat.list_devices().forEach(device => {
            if (device !== this.padDevice &&
                device.get_device_type() === Clutter.InputDeviceType.PAD_DEVICE &&
                this.padDevice.is_grouped(device))
                this._groupPads.push(device);
        });

        this.connect('destroy', this._onDestroy.bind(this));
        Main.uiGroup.add_child(this);

        this._monitorIndex = monitorIndex;
        let constraint = new Layout.MonitorConstraint({index: monitorIndex});
        this.add_constraint(constraint);

        this._titleBox = new St.BoxLayout({
            style_class: 'pad-osd-title-box',
            vertical: false,
            x_expand: false,
            x_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this._titleBox);

        const labelBox = new St.BoxLayout({
            style_class: 'pad-osd-title-menu-box',
            vertical: true,
        });
        this._titleBox.add_child(labelBox);

        this._titleLabel = new St.Label({
            style: 'font-side: larger; font-weight: bold;',
            x_align: Clutter.ActorAlign.CENTER,
        });
        this._titleLabel.clutter_text.set_ellipsize(Pango.EllipsizeMode.NONE);
        this._titleLabel.clutter_text.set_text(padDevice.get_device_name());
        labelBox.add_child(this._titleLabel);

        this._tipLabel = new St.Label({x_align: Clutter.ActorAlign.CENTER});
        labelBox.add_child(this._tipLabel);

        this._updatePadChooser();

        this._actionEditor = new ActionEditor();
        this._actionEditor.connect('done', this._endActionEdition.bind(this));

        this._padDiagram = new PadDiagram({
            image: this._imagePath,
            left_handed: settings.get_boolean('left-handed'),
            editor_actor: this._actionEditor,
            x_expand: true,
            y_expand: true,
        });
        this.add_child(this._padDiagram);
        this._updateActionLabels();

        const buttonBox = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(buttonBox);
        this._editButton = new St.Button({
            label: _('Edit…'),
            style_class: 'button',
            can_focus: true,
            x_align: Clutter.ActorAlign.CENTER,
        });
        this._editButton.connect('clicked', () => {
            this.setEditionMode(true);
        });
        buttonBox.add_child(this._editButton);

        this._syncEditionMode();
        this._grab = Main.pushModal(this);
    }

    _updatePadChooser() {
        if (this._groupPads.length > 1) {
            if (this._padChooser == null) {
                this._padChooser = new PadChooser(this.padDevice, this._groupPads);
                this._padChooser.connect('pad-selected', (chooser, pad) => {
                    this._requestForOtherPad(pad);
                });
                this._titleBox.add_child(this._padChooser);
            } else {
                this._padChooser.update(this._groupPads);
            }
        } else if (this._padChooser != null) {
            this._padChooser.destroy();
            this._padChooser = null;
        }
    }

    _requestForOtherPad(pad) {
        if (pad === this.padDevice || !this._groupPads.includes(pad))
            return;

        let editionMode = this._editionMode;
        this.destroy();
        global.display.request_pad_osd(pad, editionMode);
    }

    _getActionText(type, number, dir) {
        let str;
        if (type === Meta.PadFeatureType.RING || type === Meta.PadFeatureType.STRIP)
            str = global.display.get_pad_feature_label(this.padDevice, type, dir, number);
        else
            str = global.display.get_pad_button_label(this.padDevice, number);

        return str ?? _('None');
    }

    _updateActionLabels() {
        this._padDiagram.updateLabels(this._getActionText.bind(this));
    }

    vfunc_captured_event(event) {
        let isModeSwitch =
            (event.type() === Clutter.EventType.PAD_BUTTON_PRESS ||
             event.type() === Clutter.EventType.PAD_BUTTON_RELEASE) &&
            this.padDevice.get_mode_switch_button_group(event.get_button()) >= 0;

        if (event.type() === Clutter.EventType.PAD_BUTTON_PRESS &&
            event.get_source_device() === this.padDevice) {
            this._padDiagram.activateButton(event.get_button());

            /* Buttons that switch between modes cannot be edited */
            if (this._editionMode && !isModeSwitch)
                this._startButtonActionEdition(event.get_button());
            return Clutter.EVENT_STOP;
        } else if (event.type() === Clutter.EventType.PAD_BUTTON_RELEASE &&
                   event.get_source_device() === this.padDevice) {
            this._padDiagram.deactivateButton(event.get_button());

            if (isModeSwitch) {
                this._endActionEdition();
                this._updateActionLabels();
            }
            return Clutter.EVENT_STOP;
        } else if (event.type() === Clutter.EventType.KEY_PRESS &&
                   (!this._editionMode || event.get_key_symbol() === Clutter.KEY_Escape)) {
            if (this._editedAction != null)
                this._endActionEdition();
            else
                this.destroy();
            return Clutter.EVENT_STOP;
        } else if (event.get_source_device() === this.padDevice &&
                   event.type() === Clutter.EventType.PAD_STRIP) {
            if (this._editionMode) {
                let [retval_, number, mode] = event.get_pad_details();
                this._startStripActionEdition(number, Meta.PadDirection.UP, mode);
            }
        } else if (event.get_source_device() === this.padDevice &&
                   event.type() === Clutter.EventType.PAD_RING) {
            if (this._editionMode) {
                let [retval_, number, mode] = event.get_pad_details();
                this._startRingActionEdition(number, Meta.PadDirection.CCW, mode);
            }
        }

        // If the event comes from another pad in the same group,
        // show the OSD for it.
        if (this._groupPads.includes(event.get_source_device())) {
            this._requestForOtherPad(event.get_source_device());
            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _syncEditionMode() {
        this._editButton.set_reactive(!this._editionMode);
        this._editButton.save_easing_state();
        this._editButton.set_easing_duration(200);
        this._editButton.set_opacity(this._editionMode ? 128 : 255);
        this._editButton.restore_easing_state();

        let title;

        if (this._editionMode) {
            title = _('Press a button to configure');
            this._tipLabel.set_text(_('Press Esc to exit'));
        } else {
            title = this.padDevice.get_device_name();
            this._tipLabel.set_text(_('Press any key to exit'));
        }

        this._titleLabel.set_text(title);
    }

    _isEditedAction(type, number, dir) {
        if (!this._editedAction)
            return false;

        return this._editedAction.type === type &&
                this._editedAction.number === number &&
                this._editedAction.dir === dir;
    }

    _endActionEdition() {
        this._actionEditor.close();

        if (this._editedAction != null) {
            const {type, number, dir, mode} = this._editedAction;
            const str = this._getActionText(type, number, dir);

            const hasNextAction =
                type === Meta.PadFeatureType.RING && dir === Meta.PadDirection.CCW ||
                type === Meta.PadFeatureType.STRIP && dir === Meta.PadDirection.UP;

            this._padDiagram.stopEdition(str);
            this._editedAction = null;

            // Maybe follow up on next ring/strip direction
            if (hasNextAction) {
                if (type === Meta.PadFeatureType.RING)
                    this._startRingActionEdition(number, Meta.PadDirection.CW, mode);
                else
                    this._startStripActionEdition(number, Meta.PadDirection.DOWN, mode);
            }
        }

        this._editedActionSettings = null;
    }

    _startActionEdition(key, type, number, dir, mode) {
        if (this._isEditedAction(type, number, dir))
            return;

        this._endActionEdition();
        this._editedAction = {type, number, dir, mode};

        const settingsPath = `${this._settings.path}${key}/`;
        this._editedActionSettings = Gio.Settings.new_with_path(
            'org.gnome.desktop.peripherals.tablet.pad-button',
            settingsPath);
        this._actionEditor.setSettings(this._editedActionSettings, type);
        this._padDiagram.startEdition(type, number, dir);
    }

    _startButtonActionEdition(button) {
        let ch = String.fromCharCode('A'.charCodeAt() + button);
        let key = `button${ch}`;
        this._startActionEdition(key, null, button);
    }

    _startRingActionEdition(ring, dir, mode) {
        let ch = String.fromCharCode('A'.charCodeAt() + ring);
        const key = `ring${ch}-${dir === Meta.PadDirection.CCW ? 'ccw' : 'cw'}-mode-${mode}`;
        this._startActionEdition(key, Meta.PadFeatureType.RING, ring, dir, mode);
    }

    _startStripActionEdition(strip, dir, mode) {
        let ch = String.fromCharCode('A'.charCodeAt() + strip);
        const key = `strip${ch}-${dir === Meta.PadDirection.UP ? 'up' : 'down'}-mode-${mode}`;
        this._startActionEdition(key, Meta.PadFeatureType.STRIP, strip, dir, mode);
    }

    setEditionMode(editionMode) {
        if (this._editionMode === editionMode)
            return;

        this._editionMode = editionMode;
        this._syncEditionMode();
    }

    _onDestroy() {
        Main.popModal(this._grab);
        this._grab = null;
        this._actionEditor.close();

        this.emit('closed');
    }
});

const PadOsdIface = loadInterfaceXML('org.gnome.Shell.Wacom.PadOsd');

export class PadOsdService extends Signals.EventEmitter {
    constructor() {
        super();

        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(PadOsdIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Wacom');
        Gio.DBus.session.own_name('org.gnome.Shell.Wacom.PadOsd', Gio.BusNameOwnerFlags.REPLACE, null, null);
    }

    ShowAsync(params, invocation) {
        let [deviceNode, editionMode] = params;
        let seat = Clutter.get_default_backend().get_default_seat();
        let devices = seat.list_devices();
        let padDevice = null;

        devices.forEach(device => {
            if (deviceNode === device.get_device_node() &&
                device.get_device_type() === Clutter.InputDeviceType.PAD_DEVICE)
                padDevice = device;
        });

        if (padDevice == null) {
            invocation.return_error_literal(
                Gio.IOErrorEnum,
                Gio.IOErrorEnum.CANCELLED,
                'Invalid params');
            return;
        }

        global.display.request_pad_osd(padDevice, editionMode);
        invocation.return_value(null);
    }
}
(uuay)dwellClick.js�import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import St from 'gi://St';

import * as PanelMenu from '../panelMenu.js';

const MOUSE_A11Y_SCHEMA       = 'org.gnome.desktop.a11y.mouse';
const KEY_DWELL_CLICK_ENABLED = 'dwell-click-enabled';
const KEY_DWELL_MODE          = 'dwell-mode';
const DWELL_MODE_WINDOW       = 'window';
const DWELL_CLICK_MODES = {
    primary: {
        name: _('Single Click'),
        icon: 'pointer-primary-click-symbolic',
        type: Clutter.PointerA11yDwellClickType.PRIMARY,
    },
    double: {
        name: _('Double Click'),
        icon: 'pointer-double-click-symbolic',
        type: Clutter.PointerA11yDwellClickType.DOUBLE,
    },
    drag: {
        name: _('Drag'),
        icon: 'pointer-drag-symbolic',
        type: Clutter.PointerA11yDwellClickType.DRAG,
    },
    secondary: {
        name: _('Secondary Click'),
        icon: 'pointer-secondary-click-symbolic',
        type: Clutter.PointerA11yDwellClickType.SECONDARY,
    },
};

export const DwellClickIndicator = GObject.registerClass(
class DwellClickIndicator extends PanelMenu.Button {
    _init() {
        super._init(0.5, _('Dwell Click'));

        this._icon = new St.Icon({
            style_class: 'system-status-icon',
            icon_name: 'pointer-primary-click-symbolic',
        });
        this.add_child(this._icon);

        this._a11ySettings = new Gio.Settings({schema_id: MOUSE_A11Y_SCHEMA});
        this._a11ySettings.connect(`changed::${KEY_DWELL_CLICK_ENABLED}`, this._syncMenuVisibility.bind(this));
        this._a11ySettings.connect(`changed::${KEY_DWELL_MODE}`, this._syncMenuVisibility.bind(this));

        this._seat = Clutter.get_default_backend().get_default_seat();
        this._seat.connect('ptr-a11y-dwell-click-type-changed', this._updateClickType.bind(this));

        this._addDwellAction(DWELL_CLICK_MODES.primary);
        this._addDwellAction(DWELL_CLICK_MODES.double);
        this._addDwellAction(DWELL_CLICK_MODES.drag);
        this._addDwellAction(DWELL_CLICK_MODES.secondary);

        this._setClickType(DWELL_CLICK_MODES.primary);
        this._syncMenuVisibility();
    }

    _syncMenuVisibility() {
        this.visible =
          this._a11ySettings.get_boolean(KEY_DWELL_CLICK_ENABLED) &&
           this._a11ySettings.get_string(KEY_DWELL_MODE) === DWELL_MODE_WINDOW;

        return GLib.SOURCE_REMOVE;
    }

    _addDwellAction(mode) {
        this.menu.addAction(mode.name, this._setClickType.bind(this, mode), mode.icon);
    }

    _updateClickType(manager, clickType) {
        for (let mode in DWELL_CLICK_MODES) {
            if (DWELL_CLICK_MODES[mode].type === clickType)
                this._icon.icon_name = DWELL_CLICK_MODES[mode].icon;
        }
    }

    _setClickType(mode) {
        this._seat.set_pointer_a11y_dwell_click_type(mode.type);
        this._icon.icon_name = mode.icon;
    }
});
(uuay)searchController.js�-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import St from 'gi://St';

import * as Main from './main.js';
import * as Search from './search.js';
import * as ShellEntry from './shellEntry.js';

const FocusTrap = GObject.registerClass(
class FocusTrap extends St.Widget {
    vfunc_navigate_focus(from, direction) {
        if (direction === St.DirectionType.TAB_FORWARD ||
            direction === St.DirectionType.TAB_BACKWARD)
            return super.vfunc_navigate_focus(from, direction);
        return false;
    }
});

function getTermsForSearchString(searchString) {
    searchString = searchString.replace(/^\s+/g, '').replace(/\s+$/g, '');
    if (searchString === '')
        return [];
    return searchString.split(/\s+/);
}

export const SearchController = GObject.registerClass({
    Properties: {
        'search-active': GObject.ParamSpec.boolean(
            'search-active', 'search-active', 'search-active',
            GObject.ParamFlags.READABLE,
            false),
    },
}, class SearchController extends St.Widget {
    _init(searchEntry, showAppsButton) {
        super._init({
            name: 'searchController',
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
            visible: false,
        });

        this._showAppsButton = showAppsButton;
        this._showAppsButton.connect('notify::checked', this._onShowAppsButtonToggled.bind(this));

        this._activePage = null;

        this._searchActive = false;

        this._entry = searchEntry;
        ShellEntry.addContextMenu(this._entry);

        this._text = this._entry.clutter_text;
        this._text.connect('text-changed', this._onTextChanged.bind(this));
        this._text.connect('key-press-event', this._onKeyPress.bind(this));
        this._text.connect('key-focus-in', () => {
            this._searchResults.highlightDefault(true);
        });
        this._text.connect('key-focus-out', () => {
            this._searchResults.highlightDefault(false);
        });
        this._entry.connect('popup-menu', () => {
            if (!this._searchActive)
                return;

            this._entry.menu.close();
            this._searchResults.popupMenuDefault();
        });
        this._entry.connect('notify::mapped', this._onMapped.bind(this));
        global.stage.connectObject('notify::key-focus',
            this._onStageKeyFocusChanged.bind(this), this);

        this._entry.set_primary_icon(new St.Icon({
            style_class: 'search-entry-icon',
            icon_name: 'edit-find-symbolic',
        }));
        this._clearIcon = new St.Icon({
            style_class: 'search-entry-icon',
            icon_name: 'edit-clear-symbolic',
        });

        this._iconClickedId = 0;
        this._capturedEventId = 0;

        this._searchResults = new Search.SearchResultsView();
        this.add_child(this._searchResults);
        Main.ctrlAltTabManager.addGroup(this._entry, _('Search'), 'shell-focus-search-symbolic');

        // Since the entry isn't inside the results container we install this
        // dummy widget as the last results container child so that we can
        // include the entry in the keynav tab path
        this._focusTrap = new FocusTrap({can_focus: true});
        this._focusTrap.connect('key-focus-in', () => {
            this._entry.grab_key_focus();
        });
        this._searchResults.add_child(this._focusTrap);

        global.focus_manager.add_group(this._searchResults);

        this._stageKeyPressId = 0;
        Main.overview.connect('showing', () => {
            this._stageKeyPressId =
                global.stage.connect('key-press-event', this._onStageKeyPress.bind(this));
        });
        Main.overview.connect('hiding', () => {
            if (this._stageKeyPressId !== 0) {
                global.stage.disconnect(this._stageKeyPressId);
                this._stageKeyPressId = 0;
            }
        });
    }

    prepareToEnterOverview() {
        this.reset();
        this._setSearchActive(false);
    }

    prepareToLeaveOverview() {
        this._setSearchActive(false);
    }

    vfunc_unmap() {
        this.reset();

        super.vfunc_unmap();
    }

    _setSearchActive(searchActive) {
        if (this._searchActive === searchActive)
            return;

        this._searchActive = searchActive;
        this.notify('search-active');
    }

    _onShowAppsButtonToggled() {
        this._setSearchActive(false);
    }

    _onStageKeyPress(actor, event) {
        // Ignore events while anything but the overview has
        // pushed a modal (system modals, looking glass, ...)
        if (Main.modalCount > 1)
            return Clutter.EVENT_PROPAGATE;

        let symbol = event.get_key_symbol();

        if (symbol === Clutter.KEY_Escape) {
            if (this._searchActive)
                this.reset();
            else if (this._showAppsButton.checked)
                this._showAppsButton.checked = false;
            else
                Main.overview.hide();
            return Clutter.EVENT_STOP;
        } else if (this._shouldTriggerSearch(symbol)) {
            this.startSearch(event);
        }
        return Clutter.EVENT_PROPAGATE;
    }

    _searchCancelled() {
        this._setSearchActive(false);

        // Leave the entry focused when it doesn't have any text;
        // when replacing a selected search term, Clutter emits
        // two 'text-changed' signals, one for deleting the previous
        // text and one for the new one - the second one is handled
        // incorrectly when we remove focus
        // (https://bugzilla.gnome.org/show_bug.cgi?id=636341) */
        if (this._text.text !== '')
            this.reset();
    }

    reset() {
        // Don't drop the key focus on Clutter's side if anything but the
        // overview has pushed a modal (e.g. system modals when activated using
        // the overview).
        if (Main.modalCount <= 1)
            global.stage.set_key_focus(null);

        this._entry.text = '';

        this._text.set_cursor_visible(true);
        this._text.set_selection(0, 0);
    }

    _onStageKeyFocusChanged() {
        let focus = global.stage.get_key_focus();
        let appearFocused = this._entry.contains(focus) ||
                             this._searchResults.contains(focus);

        this._text.set_cursor_visible(appearFocused);

        if (appearFocused)
            this._entry.add_style_pseudo_class('focus');
        else
            this._entry.remove_style_pseudo_class('focus');
    }

    _onMapped() {
        if (this._entry.mapped) {
            // Enable 'find-as-you-type'
            this._capturedEventId =
                global.stage.connect('captured-event', this._onCapturedEvent.bind(this));
            this._text.set_cursor_visible(true);
            this._text.set_selection(0, 0);
        } else {
            // Disable 'find-as-you-type'
            if (this._capturedEventId > 0)
                global.stage.disconnect(this._capturedEventId);
            this._capturedEventId = 0;
        }
    }

    _shouldTriggerSearch(symbol) {
        if (symbol === Clutter.KEY_Multi_key)
            return true;

        if (symbol === Clutter.KEY_BackSpace && this._searchActive)
            return true;

        let unicode = Clutter.keysym_to_unicode(symbol);
        if (unicode === 0)
            return false;

        if (getTermsForSearchString(String.fromCharCode(unicode)).length > 0)
            return true;

        return false;
    }

    startSearch(event) {
        global.stage.set_key_focus(this._text);
        this._text.event(event, false);
    }

    // the entry does not show the hint
    _isActivated() {
        return this._text.text === this._entry.get_text();
    }

    _onTextChanged() {
        let terms = getTermsForSearchString(this._entry.get_text());

        const searchActive = terms.length > 0;
        this._searchResults.setTerms(terms);

        if (searchActive) {
            this._setSearchActive(true);

            this._entry.set_secondary_icon(this._clearIcon);

            if (this._iconClickedId === 0) {
                this._iconClickedId =
                    this._entry.connect('secondary-icon-clicked', this.reset.bind(this));
            }
        } else {
            if (this._iconClickedId > 0) {
                this._entry.disconnect(this._iconClickedId);
                this._iconClickedId = 0;
            }

            this._entry.set_secondary_icon(null);
            this._searchCancelled();
        }
    }

    _onKeyPress(entry, event) {
        let symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_Escape) {
            if (this._isActivated()) {
                this.reset();
                return Clutter.EVENT_STOP;
            }
        } else if (this._searchActive) {
            let arrowNext, nextDirection;
            if (entry.get_text_direction() === Clutter.TextDirection.RTL) {
                arrowNext = Clutter.KEY_Left;
                nextDirection = St.DirectionType.LEFT;
            } else {
                arrowNext = Clutter.KEY_Right;
                nextDirection = St.DirectionType.RIGHT;
            }

            if (symbol === Clutter.KEY_Tab) {
                this._searchResults.navigateFocus(St.DirectionType.TAB_FORWARD);
                return Clutter.EVENT_STOP;
            } else if (symbol === Clutter.KEY_ISO_Left_Tab) {
                this._focusTrap.can_focus = false;
                this._searchResults.navigateFocus(St.DirectionType.TAB_BACKWARD);
                this._focusTrap.can_focus = true;
                return Clutter.EVENT_STOP;
            } else if (symbol === Clutter.KEY_Down) {
                this._searchResults.navigateFocus(St.DirectionType.DOWN);
                return Clutter.EVENT_STOP;
            } else if (symbol === arrowNext && this._text.cursor_position === -1) {
                this._searchResults.navigateFocus(nextDirection);
                return Clutter.EVENT_STOP;
            } else if (symbol === Clutter.KEY_Return || symbol === Clutter.KEY_KP_Enter) {
                this._searchResults.activateDefault();
                return Clutter.EVENT_STOP;
            }
        }
        return Clutter.EVENT_PROPAGATE;
    }

    _onCapturedEvent(actor, event) {
        if (event.type() === Clutter.EventType.BUTTON_PRESS) {
            const targetActor = global.stage.get_event_actor(event);
            if (targetActor !== this._text &&
                this._text.has_key_focus() &&
                this._text.text === '' &&
                !this._text.has_preedit() &&
                !Main.layoutManager.keyboardBox.contains(targetActor)) {
                // the user clicked outside after activating the entry, but
                // with no search term entered and no keyboard button pressed
                // - cancel the search
                this.reset();
            }
        }

        return Clutter.EVENT_PROPAGATE;
    }

    /**
     * addProvider:
     *
     * Add a search provider to the controller.
     *
     * @param {object} provider - a search provider implementation
     */
    addProvider(provider) {
        this._searchResults._registerProvider(provider);
    }

    /**
     * removeProvider:
     *
     * Remove a search provider from the controller.
     *
     * @param {object} provider - a search provider implementation
     */
    removeProvider(provider) {
        this._searchResults._unregisterProvider(provider);
    }

    get searchActive() {
        return this._searchActive;
    }
});
(uuay)mpris.js($import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import * as Signals from '../misc/signals.js';

import * as Main from './main.js';
import * as MessageList from './messageList.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';

const DBusIface = loadInterfaceXML('org.freedesktop.DBus');
const DBusProxy = Gio.DBusProxy.makeProxyWrapper(DBusIface);

const MprisIface = loadInterfaceXML('org.mpris.MediaPlayer2');
const MprisProxy = Gio.DBusProxy.makeProxyWrapper(MprisIface);

const MprisPlayerIface = loadInterfaceXML('org.mpris.MediaPlayer2.Player');
const MprisPlayerProxy = Gio.DBusProxy.makeProxyWrapper(MprisPlayerIface);

const MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.';

export const MediaMessage = GObject.registerClass(
class MediaMessage extends MessageList.Message {
    constructor(player) {
        super(player.source);

        this._player = player;
        this.add_style_class_name('media-message');

        this._prevButton = this.addMediaControl('media-skip-backward-symbolic',
            () => {
                this._player.previous();
            });

        this._playPauseButton = this.addMediaControl('',
            () => {
                this._player.playPause();
            });

        this._nextButton = this.addMediaControl('media-skip-forward-symbolic',
            () => {
                this._player.next();
            });

        this._player.connectObject(
            'changed', this._update.bind(this),
            'closed', this.close.bind(this), this);
        this._update();
    }

    vfunc_clicked() {
        this._player.raise();
        Main.panel.closeCalendar();
    }

    _updateNavButton(button, sensitive) {
        button.reactive = sensitive;
    }

    _update() {
        let icon;
        if (this._player.trackCoverUrl) {
            const file = Gio.File.new_for_uri(this._player.trackCoverUrl);
            icon = new Gio.FileIcon({file});
        } else {
            icon = new Gio.ThemedIcon({name: 'audio-x-generic-symbolic'});
        }

        this.set({
            title: this._player.trackTitle,
            body: this._player.trackArtists.join(', '),
            icon,
        });

        let isPlaying = this._player.status === 'Playing';
        let iconName = isPlaying
            ? 'media-playback-pause-symbolic'
            : 'media-playback-start-symbolic';
        this._playPauseButton.child.icon_name = iconName;

        this._updateNavButton(this._prevButton, this._player.canGoPrevious);
        this._updateNavButton(this._nextButton, this._player.canGoNext);
    }
});

export class MprisPlayer extends Signals.EventEmitter {
    constructor(busName) {
        super();

        this._mprisProxy = new MprisProxy(Gio.DBus.session, busName,
            '/org/mpris/MediaPlayer2',
            this._onMprisProxyReady.bind(this));
        this._playerProxy = new MprisPlayerProxy(Gio.DBus.session, busName,
            '/org/mpris/MediaPlayer2',
            this._onPlayerProxyReady.bind(this));

        this._visible = false;
        this._trackArtists = [];
        this._trackTitle = '';
        this._trackCoverUrl = '';
        this._busName = busName;
        this.source = new MessageList.Source();
    }

    get status() {
        return this._playerProxy.PlaybackStatus;
    }

    get trackArtists() {
        return this._trackArtists;
    }

    get trackTitle() {
        return this._trackTitle;
    }

    get trackCoverUrl() {
        return this._trackCoverUrl;
    }

    get app() {
        return this._app;
    }

    playPause() {
        this._playerProxy.PlayPauseAsync().catch(logError);
    }

    get canGoNext() {
        return this._playerProxy.CanGoNext;
    }

    next() {
        this._playerProxy.NextAsync().catch(logError);
    }

    get canGoPrevious() {
        return this._playerProxy.CanGoPrevious;
    }

    previous() {
        this._playerProxy.PreviousAsync().catch(logError);
    }

    raise() {
        // The remote Raise() method may run into focus stealing prevention,
        // so prefer activating the app via .desktop file if possible
        if (this._app)
            this._app.activate();
        else if (this._mprisProxy.CanRaise)
            this._mprisProxy.RaiseAsync().catch(logError);
    }

    _close() {
        this._mprisProxy.disconnectObject(this);
        this._mprisProxy = null;

        this._playerProxy.disconnectObject(this);
        this._playerProxy = null;

        this.emit('closed');
    }

    _onMprisProxyReady() {
        this._mprisProxy.connectObject('notify::g-name-owner',
            () => {
                if (!this._mprisProxy.g_name_owner)
                    this._close();
            }, this);
        // It is possible for the bus to disappear before the previous signal
        // is connected, so we must ensure that the bus still exists at this
        // point.
        if (!this._mprisProxy.g_name_owner)
            this._close();
    }

    _onPlayerProxyReady() {
        this._playerProxy.connectObject(
            'g-properties-changed', () => this._updateState(), this);
        this._updateState();
    }

    _updateState() {
        let metadata = {};
        for (let prop in this._playerProxy.Metadata)
            metadata[prop] = this._playerProxy.Metadata[prop].deepUnpack();

        // Validate according to the spec; some clients send buggy metadata:
        // https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata
        this._trackArtists = metadata['xesam:artist'];
        if (!Array.isArray(this._trackArtists) ||
            !this._trackArtists.every(artist => typeof artist === 'string')) {
            if (typeof this._trackArtists !== 'undefined') {
                log(`Received faulty track artist metadata from ${
                    this._busName}; expected an array of strings, got ${
                    this._trackArtists} (${typeof this._trackArtists})`);
            }
            this._trackArtists =  [_('Unknown artist')];
        }

        this._trackTitle = metadata['xesam:title'];
        if (typeof this._trackTitle !== 'string') {
            if (typeof this._trackTitle !== 'undefined') {
                log(`Received faulty track title metadata from ${
                    this._busName}; expected a string, got ${
                    this._trackTitle} (${typeof this._trackTitle})`);
            }
            this._trackTitle = _('Unknown title');
        }

        this._trackCoverUrl = metadata['mpris:artUrl'];
        if (typeof this._trackCoverUrl !== 'string') {
            if (typeof this._trackCoverUrl !== 'undefined') {
                log(`Received faulty track cover art metadata from ${
                    this._busName}; expected a string, got ${
                    this._trackCoverUrl} (${typeof this._trackCoverUrl})`);
            }
            this._trackCoverUrl = '';
        }

        if (this._mprisProxy.DesktopEntry) {
            const desktopId = `${this._mprisProxy.DesktopEntry}.desktop`;
            this._app = Shell.AppSystem.get_default().lookup_app(desktopId);
        } else {
            this._app = null;
        }

        this.source.set({
            title: this._app?.get_name() ?? this._mprisProxy.Identity,
            icon: this._app?.get_icon() ?? null,
        });

        this.emit('changed');

        let visible = this._playerProxy.CanPlay;

        if (this._visible !== visible) {
            this._visible = visible;
            if (visible)
                this.emit('show');
            else
                this.emit('hide');
        }
    }
}

export const MediaSection = GObject.registerClass(
class MediaSection extends MessageList.MessageListSection {
    _init() {
        super._init();

        this._players = new Map();

        this._proxy = new DBusProxy(Gio.DBus.session,
            'org.freedesktop.DBus',
            '/org/freedesktop/DBus',
            this._onProxyReady.bind(this));
    }

    get allowed() {
        return !Main.sessionMode.isGreeter;
    }

    _addPlayer(busName) {
        if (this._players.get(busName))
            return;

        let player = new MprisPlayer(busName);
        let message = null;
        player.connect('closed',
            () => {
                this._players.delete(busName);
            });
        player.connect('show', () => {
            message = new MediaMessage(player);
            this.addMessage(message, true);
        });
        player.connect('hide', () => {
            this.removeMessage(message, true);
            message = null;
        });

        this._players.set(busName, player);
    }

    async _onProxyReady() {
        const [names] = await this._proxy.ListNamesAsync();
        names.forEach(name => {
            if (!name.startsWith(MPRIS_PLAYER_PREFIX))
                return;

            this._addPlayer(name);
        });
        this._proxy.connectSignal('NameOwnerChanged',
            this._onNameOwnerChanged.bind(this));
    }

    _onNameOwnerChanged(proxy, sender, [name, oldOwner, newOwner]) {
        if (!name.startsWith(MPRIS_PLAYER_PREFIX))
            return;

        if (newOwner && !oldOwner)
            this._addPlayer(name);
    }
});
(uuay)environment.js�/// Load all required dependencies with the correct versions
import '../misc/dependencies.js';

import {setConsoleLogDomain} from 'console';
import * as Gettext from 'gettext';

import Cairo from 'cairo';
import Clutter from 'gi://Clutter';
import Gdk from 'gi://Gdk';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Mtk from 'gi://Mtk';
import Polkit from 'gi://Polkit';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as SignalTracker from '../misc/signalTracker.js';
import {adjustAnimationTime} from '../misc/animationUtils.js';

setConsoleLogDomain('GNOME Shell');

Gio._promisify(Gio.DataInputStream.prototype, 'fill_async');
Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async');
Gio._promisify(Gio.DBus, 'get');
Gio._promisify(Gio.DBusConnection.prototype, 'call');
Gio._promisify(Gio.DBusProxy, 'new');
Gio._promisify(Gio.DBusProxy.prototype, 'init_async');
Gio._promisify(Gio.DBusProxy.prototype, 'call_with_unix_fd_list');
Gio._promisify(Gio.File.prototype, 'query_info_async');
Gio._promisify(Polkit.Permission, 'new');
Gio._promisify(Shell.App.prototype, 'activate_action');

// We can't import shell JS modules yet, because they may have
// variable initializations, etc, that depend on this file's
// changes

function _patchLayoutClass(layoutClass, styleProps) {
    if (styleProps) {
        layoutClass.prototype.hookup_style = function (container) {
            container.connect('style-changed', () => {
                let node = container.get_theme_node();
                for (let prop in styleProps) {
                    let [found, length] = node.lookup_length(styleProps[prop], false);
                    if (found)
                        this[prop] = length;
                }
            });
        };
    }
}

function _makeEaseCallback(params, cleanup) {
    let onComplete = params.onComplete;
    delete params.onComplete;

    let onStopped = params.onStopped;
    delete params.onStopped;

    return isFinished => {
        cleanup();

        if (onStopped)
            onStopped(isFinished);
        if (onComplete && isFinished)
            onComplete();
    };
}

function _getPropertyTarget(actor, propName) {
    if (!propName.startsWith('@'))
        return [actor, propName];

    let [type, name, prop] = propName.split('.');
    switch (type) {
    case '@layout':
        return [actor.layout_manager, name];
    case '@actions':
        return [actor.get_action(name), prop];
    case '@constraints':
        return [actor.get_constraint(name), prop];
    case '@content':
        return [actor.content, name];
    case '@effects':
        return [actor.get_effect(name), prop];
    }

    throw new Error(`Invalid property name ${propName}`);
}

function _easeActor(actor, params) {
    actor.save_easing_state();

    if (params.duration !== undefined)
        actor.set_easing_duration(params.duration);
    delete params.duration;

    if (params.delay !== undefined)
        actor.set_easing_delay(params.delay);
    delete params.delay;

    let repeatCount = 0;
    if (params.repeatCount !== undefined)
        repeatCount = params.repeatCount;
    delete params.repeatCount;

    let autoReverse = false;
    if (params.autoReverse !== undefined)
        autoReverse = params.autoReverse;
    delete params.autoReverse;

    // repeatCount doesn't include the initial iteration
    const numIterations = repeatCount + 1;
    // whether the transition should finish where it started
    const isReversed = autoReverse && numIterations % 2 === 0;

    if (params.mode !== undefined)
        actor.set_easing_mode(params.mode);
    delete params.mode;

    const prepare = () => {
        Meta.disable_unredirect_for_display(global.display);
        global.begin_work();
    };
    const cleanup = () => {
        Meta.enable_unredirect_for_display(global.display);
        global.end_work();
    };
    let callback = _makeEaseCallback(params, cleanup);

    // cancel overwritten transitions
    let animatedProps = Object.keys(params).map(p => p.replace('_', '-', 'g'));
    animatedProps.forEach(p => actor.remove_transition(p));

    if (actor.get_easing_duration() > 0 || !isReversed)
        actor.set(params);
    actor.restore_easing_state();

    const transitions = animatedProps
        .map(p => actor.get_transition(p))
        .filter(t => t !== null);

    transitions.forEach(t => t.set({repeatCount, autoReverse}));

    const [transition] = transitions;

    if (transition && transition.delay)
        transition.connect('started', () => prepare());
    else
        prepare();

    if (transition)
        transition.connect('stopped', (t, finished) => callback(finished));
    else
        callback(true);
}

function _easeActorProperty(actor, propName, target, params) {
    // Avoid pointless difference with ease()
    if (params.mode)
        params.progress_mode = params.mode;
    delete params.mode;

    if (params.duration)
        params.duration = adjustAnimationTime(params.duration);
    let duration = Math.floor(params.duration || 0);

    let repeatCount = 0;
    if (params.repeatCount !== undefined)
        repeatCount = params.repeatCount;
    delete params.repeatCount;

    let autoReverse = false;
    if (params.autoReverse !== undefined)
        autoReverse = params.autoReverse;
    delete params.autoReverse;

    // repeatCount doesn't include the initial iteration
    const numIterations = repeatCount + 1;
    // whether the transition should finish where it started
    const isReversed = autoReverse && numIterations % 2 === 0;

    // Copy Clutter's behavior for implicit animations, see
    // should_skip_implicit_transition()
    if (actor instanceof Clutter.Actor && !actor.mapped)
        duration = 0;

    const prepare = () => {
        Meta.disable_unredirect_for_display(global.display);
        global.begin_work();
    };
    const cleanup = () => {
        Meta.enable_unredirect_for_display(global.display);
        global.end_work();
    };
    let callback = _makeEaseCallback(params, cleanup);

    // cancel overwritten transition
    actor.remove_transition(propName);

    if (duration === 0) {
        let [obj, prop] = _getPropertyTarget(actor, propName);

        if (!isReversed)
            obj[prop] = target;

        prepare();
        callback(true);

        return;
    }

    let pspec = actor.find_property(propName);
    let transition = new Clutter.PropertyTransition({
        property_name: propName,
        interval: new Clutter.Interval({value_type: pspec.value_type}),
        remove_on_complete: true,
        repeat_count: repeatCount,
        auto_reverse: autoReverse,
        ...params,
    });
    actor.add_transition(propName, transition);

    transition.set_to(target);

    if (transition.delay)
        transition.connect('started', () => prepare());
    else
        prepare();

    transition.connect('stopped', (t, finished) => callback(finished));
}

// Add some bindings to the global JS namespace
globalThis.global = Shell.Global.get();

globalThis._ = Gettext.gettext;
globalThis.C_ = Gettext.pgettext;
globalThis.ngettext = Gettext.ngettext;
globalThis.N_ = s => s;

GObject.gtypeNameBasedOnJSPath = true;

GObject.Object.prototype.connectObject = function (...args) {
    SignalTracker.connectObject(this, ...args);
};
GObject.Object.prototype.connect_object = function (...args) {
    SignalTracker.connectObject(this, ...args);
};
GObject.Object.prototype.disconnectObject = function (...args) {
    SignalTracker.disconnectObject(this, ...args);
};
GObject.Object.prototype.disconnect_object = function (...args) {
    SignalTracker.disconnectObject(this, ...args);
};

SignalTracker.registerDestroyableType(Clutter.Actor);

Cairo.Context.prototype.setSourceColor = function (color) {
    const {red, green, blue, alpha} = color;
    const rgb = [red, green, blue].map(v => v / 255.0);

    if (alpha !== 0xff)
        this.setSourceRGBA(...rgb, alpha / 255.0);
    else
        this.setSourceRGB(...rgb);
};

// Miscellaneous monkeypatching
_patchLayoutClass(Clutter.GridLayout, {
    row_spacing: 'spacing-rows',
    column_spacing: 'spacing-columns',
});
_patchLayoutClass(Clutter.BoxLayout, {spacing: 'spacing'});

const origSetEasingDuration = Clutter.Actor.prototype.set_easing_duration;
Clutter.Actor.prototype.set_easing_duration = function (msecs) {
    origSetEasingDuration.call(this, adjustAnimationTime(msecs));
};
const origSetEasingDelay = Clutter.Actor.prototype.set_easing_delay;
Clutter.Actor.prototype.set_easing_delay = function (msecs) {
    origSetEasingDelay.call(this, adjustAnimationTime(msecs));
};

Clutter.Actor.prototype.ease = function (props) {
    _easeActor(this, props);
};
Clutter.Actor.prototype.ease_property = function (propName, target, params) {
    _easeActorProperty(this, propName, target, params);
};
St.Adjustment.prototype.ease = function (target, params) {
    // we're not an actor of course, but we implement the same
    // transition API as Clutter.Actor, so this works anyway
    _easeActorProperty(this, 'value', target, params);
};

Clutter.Actor.prototype[Symbol.iterator] = function* () {
    for (let c = this.get_first_child(); c; c = c.get_next_sibling())
        yield c;
};

Clutter.Actor.prototype.toString = function () {
    return St.describe_actor(this);
};
// Deprecation warning for former JS classes turned into an actor subclass
Object.defineProperty(Clutter.Actor.prototype, 'actor', {
    get() {
        let klass = this.constructor.name;
        let {stack} = new Error();
        log(`Usage of object.actor is deprecated for ${klass}\n${stack}`);
        return this;
    },
});

Meta.Rectangle = function (params = {}) {
    console.warn('Meta.Rectangle is deprecated, use Mtk.Rectangle instead');
    return new Mtk.Rectangle(params);
};

Gio.File.prototype.touch_async = function (callback) {
    Shell.util_touch_file_async(this, callback);
};
Gio.File.prototype.touch_finish = function (result) {
    return Shell.util_touch_file_finish(this, result);
};

const origToString = Object.prototype.toString;
Object.prototype.toString = function () {
    let base = origToString.call(this);
    try {
        if ('actor' in this && this.actor instanceof Clutter.Actor)
            return base.replace(/\]$/, ` delegate for ${this.actor.toString().substring(1)}`);
        else
            return base;
    } catch (e) {
        return base;
    }
};

const slowdownEnv = GLib.getenv('GNOME_SHELL_SLOWDOWN_FACTOR');
if (slowdownEnv) {
    let factor = parseFloat(slowdownEnv);
    if (!isNaN(factor) && factor > 0.0)
        St.Settings.get().slow_down_factor = factor;
}

function wrapSpawnFunction(func) {
    const originalFunc = GLib[func];
    return function (workingDirectory, argv, envp, flags, childSetup, ...args) {
        const commonArgs = [workingDirectory, argv, envp, flags];
        if (childSetup) {
            logError(new Error(`Using child GLib.${func} with a GLib.SpawnChildSetupFunc ` +
                'is unsafe and may dead-lock, thus it should never be used from JavaScript. ' +
                `Shell.${func} can be used to perform default actions or an ` +
                'async-signal-safe alternative should be used instead'));
            return originalFunc(...commonArgs, childSetup, ...args);
        }

        const retValue = Shell[`util_${func}`](...commonArgs, ...args);
        return [true, ...Array.isArray(retValue) ? retValue : [retValue]];
    };
}
GLib.spawn_async = wrapSpawnFunction('spawn_async');
GLib.spawn_async_with_pipes = wrapSpawnFunction('spawn_async_with_pipes');
GLib.spawn_async_with_fds = wrapSpawnFunction('spawn_async_with_fds');
GLib.spawn_async_with_pipes_and_fds = wrapSpawnFunction('spawn_async_with_pipes_and_fds');

// OK, now things are initialized enough that we can import shell JS
const Format = imports.format;

String.prototype.format = Format.format;

Math.clamp = function (x, lower, upper) {
    return Math.min(Math.max(x, lower), upper);
};

// Prevent extensions from opening a display connection to ourselves
Gdk.set_allowed_backends('');
(uuay)listModes.jsVimport './environment.js';

import {listModes} from './sessionMode.js';

listModes();
(uuay)fileUtils.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GLib from 'gi://GLib';
import Gio from 'gi://Gio';

export {loadInterfaceXML} from './dbusUtils.js';

/**
 * @typedef {object} SubdirInfo
 * @property {Gio.File} dir the file object for the subdir
 * @property {Gio.FileInfo} info the file descriptor for the subdir
 */

/**
 * @param {string} subdir the subdirectory to search within the data directories
 * @param {boolean} includeUserDir whether the user's data directory should also be searched in addition
 *                                 to the system data directories
 * @returns {Generator<SubdirInfo, void, void>} a generator which yields file info for subdirectories named
 *                                              `subdir` within data directories
 */
export function* collectFromDatadirs(subdir, includeUserDir) {
    let dataDirs = GLib.get_system_data_dirs();
    if (includeUserDir)
        dataDirs.unshift(GLib.get_user_data_dir());

    for (let i = 0; i < dataDirs.length; i++) {
        let path = GLib.build_filenamev([dataDirs[i], 'gnome-shell', subdir]);
        let dir = Gio.File.new_for_path(path);

        let fileEnum;
        try {
            fileEnum = dir.enumerate_children('standard::name,standard::type',
                Gio.FileQueryInfoFlags.NONE, null);
        } catch (e) {
            fileEnum = null;
        }
        if (fileEnum != null) {
            let info;
            while ((info = fileEnum.next_file(null)))
                yield {dir: fileEnum.get_child(info), info};
        }
    }
}

/**
 * @param {Gio.File} dir
 * @param {boolean} deleteParent
 */
export function recursivelyDeleteDir(dir, deleteParent) {
    let children = dir.enumerate_children('standard::name,standard::type',
        Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);

    let info;
    while ((info = children.next_file(null)) != null) {
        let type = info.get_file_type();
        let child = dir.get_child(info.get_name());
        if (type === Gio.FileType.REGULAR)
            child.delete(null);
        else if (type === Gio.FileType.DIRECTORY)
            recursivelyDeleteDir(child, true);
    }

    if (deleteParent)
        dir.delete(null);
}

/**
 * @param {Gio.File} srcDir
 * @param {Gio.File} destDir
 */
export function recursivelyMoveDir(srcDir, destDir) {
    let children = srcDir.enumerate_children('standard::name,standard::type',
        Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);

    if (!destDir.query_exists(null))
        destDir.make_directory_with_parents(null);

    let info;
    while ((info = children.next_file(null)) != null) {
        let type = info.get_file_type();
        let srcChild = srcDir.get_child(info.get_name());
        let destChild = destDir.get_child(info.get_name());
        if (type === Gio.FileType.REGULAR)
            srcChild.move(destChild, Gio.FileCopyFlags.NONE, null, null);
        else if (type === Gio.FileType.DIRECTORY)
            recursivelyMoveDir(srcChild, destChild);
    }
}
(uuay)components/���"ncomponents.js�import * as Main from './main.js';

export class ComponentManager {
    constructor() {
        this._allComponents = {};
        this._enabledComponents = [];

        Main.sessionMode.connect('updated', () => {
            this._sessionModeUpdated().catch(logError);
        });

        this._sessionModeUpdated().catch(logError);
    }

    async _sessionModeUpdated() {
        let newEnabledComponents = Main.sessionMode.components;

        await Promise.allSettled([...newEnabledComponents
            .filter(name => !this._enabledComponents.includes(name))
            .map(name => this._enableComponent(name))]);

        this._enabledComponents
            .filter(name => !newEnabledComponents.includes(name))
            .forEach(name => this._disableComponent(name));

        this._enabledComponents = newEnabledComponents;
    }

    async _importComponent(name) {
        let module = await import(`./components/${name}.js`);
        return module.Component;
    }

    async _ensureComponent(name) {
        let component = this._allComponents[name];
        if (component)
            return component;

        if (Main.sessionMode.isLocked)
            return null;

        let constructor = await this._importComponent(name);
        component = new constructor();
        this._allComponents[name] = component;
        return component;
    }

    async _enableComponent(name) {
        let component = await this._ensureComponent(name);
        if (component)
            component.enable();
    }

    _disableComponent(name) {
        let component = this._allComponents[name];
        if (component == null)
            return;
        component.disable();
    }
}
(uuay)realmd.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import * as Signals from '../misc/signals.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';

const ProviderIface = loadInterfaceXML('org.freedesktop.realmd.Provider');
const Provider = Gio.DBusProxy.makeProxyWrapper(ProviderIface);

const ServiceIface = loadInterfaceXML('org.freedesktop.realmd.Service');
const Service = Gio.DBusProxy.makeProxyWrapper(ServiceIface);

const RealmIface = loadInterfaceXML('org.freedesktop.realmd.Realm');
const Realm = Gio.DBusProxy.makeProxyWrapper(RealmIface);

export class Manager extends Signals.EventEmitter {
    constructor() {
        super();

        this._aggregateProvider = Provider(Gio.DBus.system,
            'org.freedesktop.realmd',
            '/org/freedesktop/realmd',
            this._reloadRealms.bind(this));
        this._realms = {};
        this._loginFormat = null;

        this._aggregateProvider.connectObject('g-properties-changed',
            (proxy, properties) => {
                const realmsChanged = !!properties.lookup_value('Realms', null);
                if (realmsChanged)
                    this._reloadRealms();
            }, this);
    }

    _reloadRealms() {
        let realmPaths = this._aggregateProvider.Realms;

        if (!realmPaths)
            return;

        for (let i = 0; i < realmPaths.length; i++) {
            Realm(Gio.DBus.system,
                'org.freedesktop.realmd',
                realmPaths[i],
                this._onRealmLoaded.bind(this));
        }
    }

    _reloadRealm(realm) {
        if (!realm.Configured) {
            if (this._realms[realm.get_object_path()])
                delete this._realms[realm.get_object_path()];

            return;
        }

        this._realms[realm.get_object_path()] = realm;

        this._updateLoginFormat();
    }

    _onRealmLoaded(realm, error) {
        if (error)
            return;

        this._reloadRealm(realm);

        realm.connect('g-properties-changed', (proxy, properties) => {
            const configuredChanged = !!properties.lookup_value('Configured', null);
            if (configuredChanged)
                this._reloadRealm(realm);
        });
    }

    _updateLoginFormat() {
        let newLoginFormat;

        for (let realmPath in this._realms) {
            let realm = this._realms[realmPath];
            if (realm.LoginFormats && realm.LoginFormats.length > 0) {
                newLoginFormat = realm.LoginFormats[0];
                break;
            }
        }

        if (this._loginFormat !== newLoginFormat) {
            this._loginFormat = newLoginFormat;
            this.emit('login-format-changed', newLoginFormat);
        }
    }

    get loginFormat() {
        if (this._loginFormat)
            return this._loginFormat;

        this._updateLoginFormat();

        return this._loginFormat;
    }

    release() {
        Service(Gio.DBus.system,
            'org.freedesktop.realmd',
            '/org/freedesktop/realmd',
            service => service.ReleaseAsync().catch(logError));
        this._aggregateProvider.disconnectObject(this);
        this._realms = { };
        this._updateLoginFormat();
    }
}
(uuay)audioDeviceSelection.js_import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Dialog from './dialog.js';
import * as ModalDialog from './modalDialog.js';

import * as Main from './main.js';
import {loadInterfaceXML} from '../misc/fileUtils.js';

const AudioDevice = {
    HEADPHONES: 1 << 0,
    HEADSET:    1 << 1,
    MICROPHONE: 1 << 2,
};

const AudioDeviceSelectionIface = loadInterfaceXML('org.gnome.Shell.AudioDeviceSelection');

const AudioDeviceSelectionDialog = GObject.registerClass({
    Signals: {'device-selected': {param_types: [GObject.TYPE_UINT]}},
}, class AudioDeviceSelectionDialog extends ModalDialog.ModalDialog {
    _init(devices) {
        super._init({styleClass: 'audio-device-selection-dialog'});

        this._deviceItems = {};

        this._buildLayout();

        if (devices & AudioDevice.HEADPHONES)
            this._addDevice(AudioDevice.HEADPHONES);
        if (devices & AudioDevice.HEADSET)
            this._addDevice(AudioDevice.HEADSET);
        if (devices & AudioDevice.MICROPHONE)
            this._addDevice(AudioDevice.MICROPHONE);

        if (this._selectionBox.get_n_children() < 2)
            throw new Error('Too few devices for a selection');
    }

    _buildLayout() {
        let content = new Dialog.MessageDialogContent({
            title: _('Select Audio Device'),
        });

        this._selectionBox = new St.BoxLayout({
            style_class: 'audio-selection-box',
            x_align: Clutter.ActorAlign.CENTER,
            x_expand: true,
        });
        content.add_child(this._selectionBox);

        this.contentLayout.add_child(content);

        if (Main.sessionMode.allowSettings) {
            this.addButton({
                action: this._openSettings.bind(this),
                label: _('Sound Settings'),
            });
        }
        this.addButton({
            action: () => this.close(),
            label: _('Cancel'),
            key: Clutter.KEY_Escape,
        });
    }

    _getDeviceLabel(device) {
        switch (device) {
        case AudioDevice.HEADPHONES:
            return _('Headphones');
        case AudioDevice.HEADSET:
            return _('Headset');
        case AudioDevice.MICROPHONE:
            return _('Microphone');
        default:
            return null;
        }
    }

    _getDeviceIcon(device) {
        switch (device) {
        case AudioDevice.HEADPHONES:
            return 'audio-headphones-symbolic';
        case AudioDevice.HEADSET:
            return 'audio-headset-symbolic';
        case AudioDevice.MICROPHONE:
            return 'audio-input-microphone-symbolic';
        default:
            return null;
        }
    }

    _addDevice(device) {
        const box = new St.BoxLayout({
            style_class: 'audio-selection-device-box',
            vertical: true,
        });
        box.connect('notify::height', () => {
            const laters = global.compositor.get_laters();
            laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                box.width = box.height;
                return GLib.SOURCE_REMOVE;
            });
        });

        const icon = new St.Icon({
            style_class: 'audio-selection-device-icon',
            icon_name: this._getDeviceIcon(device),
        });
        box.add_child(icon);

        const label = new St.Label({
            style_class: 'audio-selection-device-label',
            text: this._getDeviceLabel(device),
            x_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(label);

        const button = new St.Button({
            style_class: 'audio-selection-device',
            can_focus: true,
            child: box,
        });
        this._selectionBox.add_child(button);

        button.connect('clicked', () => {
            this.emit('device-selected', device);
            this.close();
            Main.overview.hide();
        });
    }

    _openSettings() {
        let desktopFile = 'gnome-sound-panel.desktop';
        let app = Shell.AppSystem.get_default().lookup_app(desktopFile);

        if (!app) {
            log(`Settings panel for desktop file ${desktopFile} could not be loaded!`);
            return;
        }

        this.close();
        Main.overview.hide();
        app.activate();
    }
});

export class AudioDeviceSelectionDBus {
    constructor() {
        this._audioSelectionDialog = null;

        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(AudioDeviceSelectionIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/AudioDeviceSelection');

        Gio.DBus.session.own_name('org.gnome.Shell.AudioDeviceSelection', Gio.BusNameOwnerFlags.REPLACE, null, null);
    }

    _onDialogClosed() {
        this._audioSelectionDialog = null;
    }

    _onDeviceSelected(dialog, device) {
        let connection = this._dbusImpl.get_connection();
        let info = this._dbusImpl.get_info();
        const deviceName = Object.keys(AudioDevice)
            .filter(dev => AudioDevice[dev] === device)[0].toLowerCase();
        connection.emit_signal(
            this._audioSelectionDialog._sender,
            this._dbusImpl.get_object_path(),
            info ? info.name : null,
            'DeviceSelected',
            GLib.Variant.new('(s)', [deviceName]));
    }

    OpenAsync(params, invocation) {
        if (this._audioSelectionDialog) {
            invocation.return_value(null);
            return;
        }

        let [deviceNames] = params;
        let devices = 0;
        deviceNames.forEach(n => (devices |= AudioDevice[n.toUpperCase()]));

        let dialog;
        try {
            dialog = new AudioDeviceSelectionDialog(devices);
        } catch (e) {
            invocation.return_value(null);
            return;
        }
        dialog._sender = invocation.get_sender();

        dialog.connect('closed', this._onDialogClosed.bind(this));
        dialog.connect('device-selected',
            this._onDeviceSelected.bind(this));
        dialog.open();

        this._audioSelectionDialog = dialog;
        invocation.return_value(null);
    }

    CloseAsync(params, invocation) {
        if (this._audioSelectionDialog &&
            this._audioSelectionDialog._sender === invocation.get_sender())
            this._audioSelectionDialog.close();

        invocation.return_value(null);
    }
}
(uuay)accessDialog.js�import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as CheckBox from './checkBox.js';
import * as Dialog from './dialog.js';
import * as ModalDialog from './modalDialog.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';

const RequestIface = loadInterfaceXML('org.freedesktop.impl.portal.Request');
const AccessIface = loadInterfaceXML('org.freedesktop.impl.portal.Access');

/** @enum {number} */
const DialogResponse = {
    OK: 0,
    CANCEL: 1,
    CLOSED: 2,
};

const AccessDialog = GObject.registerClass(
class AccessDialog extends ModalDialog.ModalDialog {
    _init(invocation, handle, title, description, body, options) {
        super._init({styleClass: 'access-dialog'});

        this._invocation = invocation;
        this._handle = handle;

        this._requestExported = false;
        this._request = Gio.DBusExportedObject.wrapJSObject(RequestIface, this);

        for (let option in options)
            options[option] = options[option].deepUnpack();

        this._buildLayout(title, description, body, options);
    }

    _buildLayout(title, description, body, options) {
        // No support for non-modal system dialogs, so ignore the option
        // let modal = options['modal'] || true;
        let denyLabel = options['deny_label'] || _('Deny');
        let grantLabel = options['grant_label'] || _('Allow');
        let choices = options['choices'] || [];

        let content = new Dialog.MessageDialogContent({title, description});
        this.contentLayout.add_child(content);

        this._choices = new Map();

        for (let i = 0; i < choices.length; i++) {
            let [id, name, opts, selected] = choices[i];
            if (opts.length > 0)
                continue; // radio buttons, not implemented

            let check = new CheckBox.CheckBox();
            check.getLabelActor().text = name;
            check.checked = selected === 'true';
            content.add_child(check);

            this._choices.set(id, check);
        }

        if (body) {
            let bodyLabel = new St.Label({
                text: body,
                x_align: Clutter.ActorAlign.CENTER,
            });
            bodyLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
            bodyLabel.clutter_text.line_wrap = true;
            content.add_child(bodyLabel);
        }

        this.addButton({
            label: denyLabel,
            action: () => this._sendResponse(DialogResponse.CANCEL),
            key: Clutter.KEY_Escape,
        });
        this.addButton({
            label: grantLabel,
            action: () => this._sendResponse(DialogResponse.OK),
        });
    }

    open() {
        if (!super.open())
            return false;

        let connection = this._invocation.get_connection();
        this._requestExported = this._request.export(connection, this._handle);
        return true;
    }

    CloseAsync(invocation, _params) {
        if (this._invocation.get_sender() !== invocation.get_sender()) {
            invocation.return_error_literal(
                Gio.DBusError,
                Gio.DBusError.ACCESS_DENIED,
                '');
            return;
        }

        this._sendResponse(DialogResponse.CLOSED);
    }

    _sendResponse(response) {
        if (this._requestExported)
            this._request.unexport();
        this._requestExported = false;

        let results = {};
        if (response === DialogResponse.OK) {
            for (let [id, check] of this._choices) {
                let checked = check.checked ? 'true' : 'false';
                results[id] = new GLib.Variant('s', checked);
            }
        }

        // Delay actual response until the end of the close animation (if any)
        this.connect('closed', () => {
            this._invocation.return_value(
                new GLib.Variant('(ua{sv})', [response, results]));
        });
        this.close();
    }
});

export class AccessDialogDBus {
    constructor() {
        this._accessDialog = null;

        this._windowTracker = Shell.WindowTracker.get_default();

        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(AccessIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/freedesktop/portal/desktop');

        Gio.DBus.session.own_name('org.gnome.Shell.Portal', Gio.BusNameOwnerFlags.REPLACE, null, null);
    }

    AccessDialogAsync(params, invocation) {
        if (this._accessDialog) {
            invocation.return_error_literal(
                Gio.DBusError,
                Gio.DBusError.LIMITS_EXCEEDED,
                'Already showing a system access dialog');
            return;
        }

        let [handle, appId, parentWindow_, title, description, body, options] = params;
        // We probably want to use parentWindow and global.display.focus_window
        // for this check in the future
        if (appId && `${appId}.desktop` !== this._windowTracker.focus_app.id) {
            invocation.return_error_literal(
                Gio.DBusError,
                Gio.DBusError.ACCESS_DENIED,
                'Only the focused app is allowed to show a system access dialog');
            return;
        }

        let dialog = new AccessDialog(
            invocation, handle, title, description, body, options);
        dialog.open();

        dialog.connect('closed', () => (this._accessDialog = null));

        this._accessDialog = dialog;
    }
}
(uuay)loginDialog.js(�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/*
 * Copyright 2011 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

import AccountsService from 'gi://AccountsService';
import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import Gdm from 'gi://Gdm';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as AuthMenuButton from './authMenuButton.js';
import * as AuthPrompt from './authPrompt.js';
import * as Batch from './batch.js';
import * as CtrlAltTab from '../ui/ctrlAltTab.js';
import * as GdmUtil from './util.js';
import * as Layout from '../ui/layout.js';
import * as LoginManager from '../misc/loginManager.js';
import * as Main from '../ui/main.js';
import * as MessageTray from '../ui/messageTray.js';
import * as ModalDialog from '../ui/modalDialog.js';
import * as Params from '../misc/params.js';
import * as Realmd from './realmd.js';
import * as UserWidget from '../ui/userWidget.js';

const _FADE_ANIMATION_TIME = 250;
const _SCROLL_ANIMATION_TIME = 500;
const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0;
const _CONFLICTING_SESSION_DIALOG_TIMEOUT = 60;

export const UserListItem = GObject.registerClass({
    Signals: {'activate': {}},
}, class UserListItem extends St.Button {
    _init(user) {
        let layout = new St.BoxLayout({
            vertical: true,
            x_expand: true,
        });
        super._init({
            style_class: 'login-dialog-user-list-item',
            button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
            can_focus: true,
            x_expand: true,
            child: layout,
            reactive: true,
        });

        this.user = user;
        this.user.connectObject('changed', this._onUserChanged.bind(this), this);

        this.connect('notify::hover', () => {
            this._setSelected(this.hover);
        });

        this._userWidget = new UserWidget.UserWidget(this.user);
        layout.add_child(this._userWidget);

        this._userWidget.bind_property('label-actor',
            this, 'label-actor',
            GObject.BindingFlags.SYNC_CREATE);

        this._timedLoginIndicator = new St.Bin({
            style_class: 'login-dialog-timed-login-indicator',
            scale_x: 0,
            visible: false,
        });
        layout.add_child(this._timedLoginIndicator);

        this._onUserChanged();
    }

    vfunc_key_focus_in() {
        super.vfunc_key_focus_in();
        this._setSelected(true);
    }

    vfunc_key_focus_out() {
        super.vfunc_key_focus_out();
        this._setSelected(false);
    }

    _onUserChanged() {
        this._updateLoggedIn();
    }

    _updateLoggedIn() {
        if (this.user.is_logged_in())
            this.add_style_pseudo_class('logged-in');
        else
            this.remove_style_pseudo_class('logged-in');
    }

    vfunc_clicked() {
        this.emit('activate');
    }

    _setSelected(selected) {
        if (selected) {
            this.add_style_pseudo_class('selected');
            this.grab_key_focus();
        } else {
            this.remove_style_pseudo_class('selected');
        }
    }

    showTimedLoginIndicator(time) {
        let hold = new Batch.Hold();

        this.hideTimedLoginIndicator();

        this._timedLoginIndicator.visible = true;

        let startTime = GLib.get_monotonic_time();

        this._timedLoginTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 33,
            () => {
                let currentTime = GLib.get_monotonic_time();
                let elapsedTime = (currentTime - startTime) / GLib.USEC_PER_SEC;
                this._timedLoginIndicator.scale_x = elapsedTime / time;
                if (elapsedTime >= time) {
                    this._timedLoginTimeoutId = 0;
                    hold.release();
                    return GLib.SOURCE_REMOVE;
                }

                return GLib.SOURCE_CONTINUE;
            });

        GLib.Source.set_name_by_id(this._timedLoginTimeoutId, '[gnome-shell] this._timedLoginTimeoutId');

        return hold;
    }

    hideTimedLoginIndicator() {
        if (this._timedLoginTimeoutId) {
            GLib.source_remove(this._timedLoginTimeoutId);
            this._timedLoginTimeoutId = 0;
        }

        this._timedLoginIndicator.visible = false;
        this._timedLoginIndicator.scale_x = 0.;
    }
});

const UserList = GObject.registerClass({
    Signals: {
        'activate': {param_types: [UserListItem.$gtype]},
        'item-added': {param_types: [UserListItem.$gtype]},
    },
}, class UserList extends St.ScrollView {
    _init() {
        super._init({
            style_class: 'login-dialog-user-list-view',
            x_expand: true,
            y_expand: true,
        });

        this._box = new St.BoxLayout({
            vertical: true,
            style_class: 'login-dialog-user-list',
            pseudo_class: 'expanded',
        });

        this.child = this._box;
        this._items = {};
    }

    vfunc_key_focus_in() {
        super.vfunc_key_focus_in();
        this._moveFocusToItems();
    }

    _moveFocusToItems() {
        let hasItems = Object.keys(this._items).length > 0;

        if (!hasItems)
            return;

        if (global.stage.get_key_focus() !== this)
            return;

        let focusSet = this.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
        if (!focusSet) {
            const laters = global.compositor.get_laters();
            laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                this._moveFocusToItems();
                return false;
            });
        }
    }

    _onItemActivated(activatedItem) {
        this.emit('activate', activatedItem);
    }

    updateStyle(isExpanded) {
        if (isExpanded)
            this._box.add_style_pseudo_class('expanded');
        else
            this._box.remove_style_pseudo_class('expanded');

        for (let userName in this._items) {
            let item = this._items[userName];
            item.sync_hover();
        }
    }

    scrollToItem(item) {
        let box = item.get_allocation_box();

        const adjustment = this.vadjustment;

        let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0);
        adjustment.ease(value, {
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            duration: _SCROLL_ANIMATION_TIME,
        });
    }

    jumpToItem(item) {
        let box = item.get_allocation_box();

        const adjustment = this.vadjustment;

        let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0);

        adjustment.set_value(value);
    }

    getItemFromUserName(userName) {
        let item = this._items[userName];

        if (!item)
            return null;

        return item;
    }

    containsUser(user) {
        return this._items[user.get_user_name()] != null;
    }

    addUser(user) {
        if (!user.is_loaded)
            return;

        if (user.is_system_account())
            return;

        if (user.locked)
            return;

        let userName = user.get_user_name();

        if (!userName)
            return;

        this.removeUser(user);

        let item = new UserListItem(user);
        this._box.add_child(item);

        this._items[userName] = item;

        item.connect('activate', this._onItemActivated.bind(this));

        // Try to keep the focused item front-and-center
        item.connect('key-focus-in', () => this.scrollToItem(item));

        this._moveFocusToItems();

        this.emit('item-added', item);
    }

    removeUser(user) {
        if (!user.is_loaded)
            return;

        let userName = user.get_user_name();

        if (!userName)
            return;

        let item = this._items[userName];

        if (!item)
            return;

        item.destroy();
        delete this._items[userName];
    }

    numItems() {
        return Object.keys(this._items).length;
    }
});

const SessionMenuButton = GObject.registerClass({
    Signals: {'session-activated': {param_types: [GObject.TYPE_STRING]}},
}, class SessionMenuButton extends St.Bin {
    _init() {
        let button = new St.Button({
            style_class: 'login-dialog-button login-dialog-session-list-button',
            icon_name: 'emblem-system-symbolic',
            reactive: true,
            track_hover: true,
            can_focus: true,
            accessible_name: _('Choose Session'),
            accessible_role: Atk.Role.MENU,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });

        super._init({child: button});
        this._button = button;

        this._menu = new PopupMenu.PopupMenu(this._button, 0, St.Side.BOTTOM);
        Main.uiGroup.add_child(this._menu.actor);
        this._menu.actor.hide();

        this._menu.connect('open-state-changed', (menu, isOpen) => {
            if (isOpen)
                this._button.add_style_pseudo_class('active');
            else
                this._button.remove_style_pseudo_class('active');
        });

        this._manager = new PopupMenu.PopupMenuManager(this._button,
            {actionMode: Shell.ActionMode.NONE});
        this._manager.addMenu(this._menu);

        this._button.connect('clicked', () => this._menu.toggle());

        this._items = {};
        this._activeSessionId = null;
        this._populate();
    }

    updateSensitivity(sensitive) {
        this._button.reactive = sensitive;
        this._button.can_focus = sensitive;
        this.opacity = sensitive ? 255 : 0;
        this._menu.close(BoxPointer.PopupAnimation.NONE);
    }

    _updateOrnament() {
        let itemIds = Object.keys(this._items);
        for (let i = 0; i < itemIds.length; i++) {
            if (itemIds[i] === this._activeSessionId)
                this._items[itemIds[i]].setOrnament(PopupMenu.Ornament.DOT);
            else
                this._items[itemIds[i]].setOrnament(PopupMenu.Ornament.NO_DOT);
        }
    }

    setActiveSession(sessionId) {
        if (sessionId === this._activeSessionId)
            return;

        this._activeSessionId = sessionId;
        this._updateOrnament();
    }

    close() {
        this._menu.close();
    }

    _populate() {
        let ids = Gdm.get_session_ids();
        ids.sort();

        if (ids.length <= 1) {
            this._button.hide();
            return;
        }

        for (let i = 0; i < ids.length; i++) {
            let [sessionName, sessionDescription_] = Gdm.get_session_name_and_description(ids[i]);

            let id = ids[i];
            let item = new PopupMenu.PopupMenuItem(sessionName);
            this._menu.addMenuItem(item);
            this._items[id] = item;

            item.connect('activate', () => {
                this.setActiveSession(id);
                this.emit('session-activated', this._activeSessionId);
            });
        }
    }
});

export const ConflictingSessionDialog = GObject.registerClass({
    Signals: {
        'cancel': {},
        'force-stop': {},
    },
}, class ConflictingSessionDialog extends ModalDialog.ModalDialog {
    _init(conflictingSession, greeterSession, userName) {
        super._init();

        let bannerText;
        if (greeterSession.Remote && conflictingSession.Remote)
            /* Translators: is running for <username> */
            bannerText = _('Remote login is not possible because a remote session is already running for %s. To login remotely, you must log out from the remote session or force stop it.').format(userName);
        else if (!greeterSession.Remote && conflictingSession.Remote)
            /* Translators: is running for <username> */
            bannerText = _('Login is not possible because a remote session is already running for %s. To login, you must log out from the remote session or force stop it.').format(userName);
        else if (greeterSession.Remote && !conflictingSession.Remote)
            /* Translators: is running for <username> */
            bannerText = _('Remote login is not possible because a local session is already running for %s. To login remotely, you must log out from the local session or force stop it.').format(userName);
        else
            /* Translators: is running for <username> */
            bannerText = _('Login is not possible because a session is already running for %s. To login, you must log out from the session or force stop it.').format(userName);

        let textLayout = new St.BoxLayout({
            style_class: 'conflicting-session-dialog-content',
            vertical: true,
            x_expand: true,
        });

        let title = new St.Label({
            text: _('Session Already Running'),
            style_class: 'conflicting-session-dialog-title',
        });

        let banner = new St.Label({
            text: bannerText,
            style_class: 'conflicting-session-dialog-desc',
        });
        banner.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        banner.clutter_text.line_wrap = true;

        let warningBanner = new St.Label({
            text: _('Force stopping will quit any running apps and processes, and could result in data loss.'),
            style_class: 'conflicting-session-dialog-desc-warning',
        });
        warningBanner.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        warningBanner.clutter_text.line_wrap = true;

        textLayout.add_child(title);
        textLayout.add_child(banner);
        textLayout.add_child(warningBanner);
        this.contentLayout.add_child(textLayout);

        this.addButton({
            label: _('Cancel'),
            action: () => {
                this.emit('cancel');
            },
            key: Clutter.KEY_Escape,
            default: true,
        });
        this.addButton({
            label: _('Force Stop'),
            action: () => {
                this.emit('force-stop');
            },
        });
    }
});

export const LoginDialog = GObject.registerClass({
    Signals: {
        'failed': {},
        'wake-up-screen': {},
    },
}, class LoginDialog extends St.Widget {
    _init(parentActor) {
        super._init({style_class: 'login-dialog', visible: false});

        this.get_accessible().set_role(Atk.Role.WINDOW);

        this.add_constraint(new Layout.MonitorConstraint({primary: true}));
        this.connect('destroy', this._onDestroy.bind(this));
        parentActor.add_child(this);

        this._userManager = AccountsService.UserManager.get_default();
        this._gdmClient = new Gdm.Client();

        try {
            this._gdmClient.set_enabled_extensions([
                Gdm.UserVerifierChoiceList.interface_info().name,
                Gdm.UserVerifierCustomJSON.interface_info().name,
            ]);
        } catch (e) {
        }

        this._settings = new Gio.Settings({schema_id: GdmUtil.LOGIN_SCREEN_SCHEMA});

        this._settings.connect(`changed::${GdmUtil.BANNER_MESSAGE_KEY}`,
            this._updateBanner.bind(this));
        this._settings.connect(`changed::${GdmUtil.BANNER_MESSAGE_TEXT_KEY}`,
            this._updateBanner.bind(this));
        this._settings.connect(`changed::${GdmUtil.DISABLE_USER_LIST_KEY}`,
            this._updateDisableUserList.bind(this));
        this._settings.connect(`changed::${GdmUtil.LOGO_KEY}`,
            this._updateLogo.bind(this));

        this._textureCache = St.TextureCache.get_default();
        this._textureCache.connectObject('texture-file-changed',
            this._updateLogoTexture.bind(this), this);

        this._userSelectionBox = new St.BoxLayout({
            style_class: 'login-dialog-user-selection-box',
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
            vertical: true,
            visible: false,
        });
        this.add_child(this._userSelectionBox);

        this._userList = new UserList();
        this._userSelectionBox.add_child(this._userList);

        this._authPrompt = new AuthPrompt.AuthPrompt(this._gdmClient, AuthPrompt.AuthPromptMode.UNLOCK_OR_LOG_IN);
        this._authPrompt.connect('prompted', this._onPrompted.bind(this));
        this._authPrompt.connect('reset', this._onReset.bind(this));
        this._authPrompt.connect('mechanisms-changed', this._onMechanismsChanged.bind(this));
        this._authPrompt.connectObject('notify::verification-status',
            () => this._updateCancelButton(), this);
        this._authPrompt.hide();
        this.add_child(this._authPrompt);

        // translators: this message is shown below the user list on the
        // login screen. It can be activated to reveal an entry for
        // manually entering the username.
        let notListedLabel = new St.Label({
            text: _('Not listed?'),
            style_class: 'login-dialog-not-listed-label',
        });
        this._notListedButton = new St.Button({
            style_class: 'login-dialog-not-listed-button',
            button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
            can_focus: true,
            child: notListedLabel,
            reactive: true,
            x_align: Clutter.ActorAlign.START,
            label_actor: notListedLabel,
        });

        this._notListedButton.connect('clicked', this._hideUserListAskForUsernameAndBeginVerification.bind(this));

        this._notListedButton.hide();

        this._userSelectionBox.add_child(this._notListedButton);

        const bannerBox = new St.BoxLayout({vertical: true});

        this._bannerView = new St.ScrollView({
            style_class: 'login-dialog-banner-view',
            opacity: 0,
            child: bannerBox,
        });
        this.add_child(this._bannerView);

        this._bannerLabel = new St.Label({
            style_class: 'login-dialog-banner',
            text: '',
        });
        this._bannerLabel.clutter_text.line_wrap = true;
        this._bannerLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        bannerBox.add_child(this._bannerLabel);
        this._updateBanner();

        this._menuButtonBox = new St.BoxLayout({style_class: 'login-dialog-menu-button-box'});
        this.add_child(this._menuButtonBox);

        this._createLoginOptionsButton();
        this._createSessionMenuButton();

        this._logoBin = new St.Widget({
            style_class: 'login-dialog-logo-bin',
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.END,
        });
        this._logoBin.connect('resource-scale-changed', () => {
            this._updateLogoTexture(this._textureCache, this._logoFile);
        });
        this.add_child(this._logoBin);
        this._updateLogo();

        this._userList.connect('activate', (userList, item) => {
            this._onUserListActivated(item);
        });

        this._disableUserList = undefined;
        this._userListLoaded = false;

        this._realmManager = new Realmd.Manager();
        this._realmManager.connectObject('login-format-changed',
            this._showRealmLoginHint.bind(this), this);

        this._getGreeterSessionProxy();

        // If the user list is enabled, it should take key focus; make sure the
        // screen shield is initialized first to prevent it from stealing the
        // focus later
        Main.layoutManager.connectObject('startup-complete',
            this._updateDisableUserList.bind(this), this);
    }

    _createLoginOptionsButton() {
        this._loginOptionsButton = new AuthMenuButton.AuthMenuButton({
            title: _('Login Options'),
            iconName: 'dialog-password-symbolic',
        });
        this._loginOptionsButton.connect('active-item-changed',
            () => {
                const activeMechanism = this._loginOptionsButton.getActiveItem();
                if (activeMechanism)
                    this._authPrompt.setForegroundMechanism(activeMechanism);
            });
        this._loginOptionsButton.opacity = 0;
        this._loginOptionsButton.show();
        this._menuButtonBox.add_child(this._loginOptionsButton);
    }

    _createSessionMenuButton() {
        this._sessionMenuButton = new AuthMenuButton.AuthMenuButton({
            title: _('Session'),
            iconName: 'emblem-system-symbolic',
        });

        let ids = Gdm.get_session_ids();
        ids.sort();

        for (const id of ids) {
            let [sessionName, sessionDescription_] = Gdm.get_session_name_and_description(id);

            this._sessionMenuButton.addItem({name: sessionName, id});
        }

        if (ids.length <= 1) {
            this._sessionMenuButton.hide();
            return;
        }

        this._sessionMenuButton.connect('active-item-changed', () => {
            const session = this._sessionMenuButton.getActiveItem();

            if (session)
                this._greeter.call_select_session_sync(session.id, null);
        });
        this._menuButtonBox.add_child(this._sessionMenuButton);
    }

    _getBannerAllocation(dialogBox) {
        let actorBox = new Clutter.ActorBox();

        let [, , natWidth, natHeight] = this._bannerView.get_preferred_size();
        let centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2;

        actorBox.x1 = Math.floor(centerX - natWidth / 2);
        actorBox.y1 = dialogBox.y1 + Main.layoutManager.panelBox.height;
        actorBox.x2 = actorBox.x1 + natWidth;
        actorBox.y2 = actorBox.y1 + natHeight;

        return actorBox;
    }

    _getLogoBinAllocation(dialogBox) {
        let actorBox = new Clutter.ActorBox();

        let [, , natWidth, natHeight] = this._logoBin.get_preferred_size();
        let centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2;

        actorBox.x1 = Math.floor(centerX - natWidth / 2);
        actorBox.y1 = dialogBox.y2 - natHeight;
        actorBox.x2 = actorBox.x1 + natWidth;
        actorBox.y2 = actorBox.y1 + natHeight;

        return actorBox;
    }

    _getMenuButtonAllocation(dialogBox) {
        let actorBox = new Clutter.ActorBox();

        let [, , natWidth, natHeight] = this._menuButtonBox.get_preferred_size();

        if (this.get_text_direction() === Clutter.TextDirection.RTL)
            actorBox.x1 = dialogBox.x1 + natWidth;
        else
            actorBox.x1 = dialogBox.x2 - natWidth;

        actorBox.y1 = dialogBox.y2 - natHeight;
        actorBox.x2 = actorBox.x1 + natWidth;
        actorBox.y2 = actorBox.y1 + natHeight;

        return actorBox;
    }

    _getCenterActorAllocation(dialogBox, actor) {
        let actorBox = new Clutter.ActorBox();

        let [, , natWidth, natHeight] = actor.get_preferred_size();
        let centerX = dialogBox.x1 + (dialogBox.x2 - dialogBox.x1) / 2;
        let centerY = dialogBox.y1 + (dialogBox.y2 - dialogBox.y1) / 2;

        natWidth = Math.min(natWidth, dialogBox.x2 - dialogBox.x1);
        natHeight = Math.min(natHeight, dialogBox.y2 - dialogBox.y1);

        actorBox.x1 = Math.floor(centerX - natWidth / 2);
        actorBox.y1 = Math.floor(centerY - natHeight / 2);
        actorBox.x2 = actorBox.x1 + natWidth;
        actorBox.y2 = actorBox.y1 + natHeight;

        return actorBox;
    }

    vfunc_allocate(dialogBox) {
        this.set_allocation(dialogBox);

        let themeNode = this.get_theme_node();
        dialogBox = themeNode.get_content_box(dialogBox);

        let dialogWidth = dialogBox.x2 - dialogBox.x1;
        let dialogHeight = dialogBox.y2 - dialogBox.y1;

        // First find out what space the children require
        let bannerAllocation = null;
        let bannerHeight = 0;
        if (this._bannerView.visible) {
            bannerAllocation = this._getBannerAllocation(dialogBox);
            bannerHeight = bannerAllocation.y2 - bannerAllocation.y1;
        }

        let authPromptAllocation = null;
        let authPromptWidth = 0;
        if (this._authPrompt.visible) {
            authPromptAllocation = this._getCenterActorAllocation(dialogBox, this._authPrompt);
            authPromptWidth = authPromptAllocation.x2 - authPromptAllocation.x1;
        }

        let userSelectionAllocation = null;
        let userSelectionHeight = 0;
        if (this._userSelectionBox.visible) {
            userSelectionAllocation = this._getCenterActorAllocation(dialogBox, this._userSelectionBox);
            userSelectionHeight = userSelectionAllocation.y2 - userSelectionAllocation.y1;
        }

        let logoAllocation = null;
        let logoHeight = 0;
        if (this._logoBin.visible) {
            logoAllocation = this._getLogoBinAllocation(dialogBox);
            logoHeight = logoAllocation.y2 - logoAllocation.y1;
        }

        let menuButtonBoxAllocation = null;
        if (this._menuButtonBox.visible)
            menuButtonBoxAllocation = this._getMenuButtonAllocation(dialogBox);


        // Then figure out if we're overly constrained and need to
        // try a different layout, or if we have what extra space we
        // can hand out
        if (bannerAllocation) {
            let bannerSpace;

            if (authPromptAllocation)
                bannerSpace = authPromptAllocation.y1 - bannerAllocation.y1;
            else
                bannerSpace = 0;

            let leftOverYSpace = bannerSpace - bannerHeight;

            if (leftOverYSpace > 0) {
                // First figure out how much left over space is up top
                let leftOverTopSpace = leftOverYSpace / 2;

                // Then, shift the banner into the middle of that extra space
                let yShift = Math.floor(leftOverTopSpace / 2);

                bannerAllocation.y1 += yShift;
                bannerAllocation.y2 += yShift;
            } else {
                // Then figure out how much space there would be if we switched to a
                // wide layout with banner on one side and authprompt on the other.
                let leftOverXSpace = dialogWidth - authPromptWidth;

                // In a wide view, half of the available space goes to the banner,
                // and the other half goes to the margins.
                let wideBannerWidth = leftOverXSpace / 2;
                let wideSpacing  = leftOverXSpace - wideBannerWidth;

                // If we do go with a wide layout, we need there to be at least enough
                // space for the banner and the auth prompt to be the same width,
                // so it doesn't look unbalanced.
                if (authPromptWidth > 0 && wideBannerWidth > authPromptWidth) {
                    let centerX = dialogBox.x1 + dialogWidth / 2;
                    let centerY = dialogBox.y1 + dialogHeight / 2;

                    // A small portion of the spacing goes down the center of the
                    // screen to help delimit the two columns of the wide view
                    let centerGap = wideSpacing / 8;

                    // place the banner along the left edge of the center margin
                    bannerAllocation.x2 = Math.floor(centerX - centerGap / 2);
                    bannerAllocation.x1 = Math.floor(bannerAllocation.x2 - wideBannerWidth);

                    // figure out how tall it would like to be and try to accommodate
                    // but don't let it get too close to the logo
                    let [, wideBannerHeight] = this._bannerView.get_preferred_height(wideBannerWidth);

                    let maxWideHeight = dialogHeight - 3 * logoHeight;
                    wideBannerHeight = Math.min(maxWideHeight, wideBannerHeight);
                    bannerAllocation.y1 = Math.floor(centerY - wideBannerHeight / 2);
                    bannerAllocation.y2 = bannerAllocation.y1 + wideBannerHeight;

                    // place the auth prompt along the right edge of the center margin
                    authPromptAllocation.x1 = Math.floor(centerX + centerGap / 2);
                    authPromptAllocation.x2 = authPromptAllocation.x1 + authPromptWidth;
                } else {
                    // If we aren't going to do a wide view, then we need to limit
                    // the height of the banner so it will present scrollbars

                    // First figure out how much space there is without the banner
                    leftOverYSpace += bannerHeight;

                    // Then figure out how much of that space is up top
                    let availableTopSpace = Math.floor(leftOverYSpace / 2);

                    // Then give all of that space to the banner
                    bannerAllocation.y2 = bannerAllocation.y1 + availableTopSpace;
                }
            }
        } else if (userSelectionAllocation) {
            // Grow the user list to fill the space
            let leftOverYSpace = dialogHeight - userSelectionHeight - logoHeight;

            if (leftOverYSpace > 0) {
                let topExpansion = Math.floor(leftOverYSpace / 2);
                let bottomExpansion = topExpansion;

                userSelectionAllocation.y1 -= topExpansion;
                userSelectionAllocation.y2 += bottomExpansion;
            }
        }

        // Finally hand out the allocations
        if (bannerAllocation)
            this._bannerView.allocate(bannerAllocation);

        if (authPromptAllocation)
            this._authPrompt.allocate(authPromptAllocation);

        if (userSelectionAllocation)
            this._userSelectionBox.allocate(userSelectionAllocation);

        if (logoAllocation)
            this._logoBin.allocate(logoAllocation);

        if (menuButtonBoxAllocation)
            this._menuButtonBox.allocate(menuButtonBoxAllocation);
    }

    _ensureUserListLoaded() {
        if (!this._userManager.is_loaded) {
            this._userManager.connectObject('notify::is-loaded',
                () => {
                    if (this._userManager.is_loaded) {
                        this._userManager.disconnectObject(this);
                        this._loadUserList();
                    }
                });
        } else {
            let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._loadUserList.bind(this));
            GLib.Source.set_name_by_id(id, '[gnome-shell] _loadUserList');
        }
    }

    _updateDisableUserList() {
        let disableUserList = this._settings.get_boolean(GdmUtil.DISABLE_USER_LIST_KEY);

        // Disable user list when there are no users.
        if (this._userListLoaded && this._userList.numItems() === 0)
            disableUserList = true;

        if (disableUserList !== this._disableUserList) {
            this._disableUserList = disableUserList;

            if (this._authPrompt.verificationStatus === AuthPrompt.AuthPromptStatus.NOT_VERIFYING)
                this._authPrompt.reset();

            if (this._disableUserList && this._timedLoginUserListHold)
                this._timedLoginUserListHold.release();
        }
    }

    _updateCancelButton() {
        let cancelVisible;

        // Hide the cancel button if the user list is disabled and we're asking for
        // a username
        if (this._authPrompt.verificationStatus === AuthPrompt.AuthPromptStatus.NOT_VERIFYING &&
            this._disableUserList)
            cancelVisible = false;
        else
            cancelVisible = true;

        this._authPrompt.cancelButton.visible = cancelVisible;
    }

    _updateBanner() {
        let enabled = this._settings.get_boolean(GdmUtil.BANNER_MESSAGE_KEY);
        let text = this._settings.get_string(GdmUtil.BANNER_MESSAGE_TEXT_KEY);

        if (enabled && text) {
            this._bannerLabel.set_text(text);
            this._bannerLabel.show();
        } else {
            this._bannerLabel.hide();
        }
    }

    _fadeInBannerView() {
        this._bannerView.show();
        this._bannerView.ease({
            opacity: 255,
            duration: _FADE_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    _hideBannerView() {
        this._bannerView.remove_all_transitions();
        this._bannerView.opacity = 0;
        this._bannerView.hide();
    }

    _updateLogoTexture(cache, file) {
        if (this._logoFile && !this._logoFile.equal(file))
            return;

        this._logoBin.destroy_all_children();
        const resourceScale = this._logoBin.get_resource_scale();
        if (this._logoFile) {
            const scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
            const texture = this._textureCache.load_file_async(
                this._logoFile,
                -1, -1,
                scaleFactor, resourceScale);
            this._logoBin.add_child(texture);
        }
    }

    _updateLogo() {
        let path = this._settings.get_string(GdmUtil.LOGO_KEY);

        this._logoFile = path ? Gio.file_new_for_path(path) : null;
        this._updateLogoTexture(this._textureCache, this._logoFile);
    }

    _onPrompted() {
        const showSessionMenu = this._shouldShowSessionMenuButton();

        this._sessionMenuButton.updateSensitivity(showSessionMenu);
        this._sessionMenuButton.visible = showSessionMenu;
        this._showPrompt();
    }

    _resetGreeterProxy() {
        if (GLib.getenv('GDM_GREETER_TEST') !== '1') {
            if (this._greeter)
                this._greeter.run_dispose();

            this._greeter = this._gdmClient.get_greeter_sync(null);

            this._greeter.connectObject(
                'default-session-name-changed', this._onDefaultSessionChanged.bind(this),
                'session-opened', this._onSessionOpened.bind(this),
                'timed-login-requested', this._onTimedLoginRequested.bind(this), this);
        }
    }

    _onReset(authPrompt, beginRequest) {
        this._resetGreeterProxy();
        this._sessionMenuButton.updateSensitivity(true);

        const previousUser = this._user;
        this._user = null;

        if (this._nextSignalId) {
            this._authPrompt.disconnect(this._nextSignalId);
            this._nextSignalId = 0;
        }

        if (previousUser && beginRequest === AuthPrompt.BeginRequestType.REUSE_USERNAME) {
            this._user = previousUser;
            this._authPrompt.setUser(this._user);
            this._beginVerification({userName: previousUser.get_user_name()});
        } else if (beginRequest === AuthPrompt.BeginRequestType.PROVIDE_USERNAME) {
            if (!this._disableUserList)
                this._showUserList();
            else
                this._hideUserListAskForUsernameAndBeginVerification();
        } else {
            this._hideUserListAndBeginVerification();
        }
    }

    _onMechanismsChanged(authPrompt, serviceName) {
        const mechanisms = Array.from(authPrompt.mechanisms.get(serviceName));

        const activeMechanism = this._loginOptionsButton.getActiveItem();

        this._loginOptionsButton.clearItems({serviceName});
        this._loginOptionsButton.updateSensitivity(mechanisms.length !== 0);
        if (mechanisms.length === 0)
            return;

        const defaultId = mechanisms[0].id;

        mechanisms.sort((a, b) => a.name.localeCompare(b.name));

        for (const {role, id, name, selectable, protocol} of mechanisms) {
            if (!selectable)
                continue;

            const mechanism = {serviceName, id, name, role, protocol};
            this._loginOptionsButton.addItem(mechanism);

            const wasActive = activeMechanism?.id === id;
            const isDefault = id === defaultId;

            if (wasActive || (!activeMechanism && isDefault))
                this._loginOptionsButton.setActiveItem(mechanism);
        }
    }

    _onDefaultSessionChanged(client, sessionId) {
        this._sessionMenuButton.setActiveItem({id: sessionId});
    }

    _shouldShowSessionMenuButton() {
        if (this._authPrompt.verificationStatus !== AuthPrompt.AuthPromptStatus.VERIFYING &&
            this._authPrompt.verificationStatus !== AuthPrompt.AuthPromptStatus.VERIFICATION_FAILED)
            return false;

        if (this._user && this._user.is_loaded && this._user.is_logged_in())
            return false;

        return true;
    }

    _showPrompt() {
        if (this._authPrompt.visible)
            return;
        this._authPrompt.opacity = 0;
        this._authPrompt.show();
        this._authPrompt.ease({
            opacity: 255,
            duration: _FADE_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
        this._fadeInBannerView();
    }

    _showRealmLoginHint(realmManager, hint) {
        if (!hint)
            return;

        hint = hint.replace(/%U/g, 'user');
        hint = hint.replace(/%D/g, 'DOMAIN');
        hint = hint.replace(/%[^UD]/g, '');

        // Translators: this message is shown below the username entry field
        // to clue the user in on how to login to the local network realm
        this._authPrompt.setMessage(_('(e.g., user or %s)').format(hint), GdmUtil.MessageType.HINT);
    }

    _askForUsernameAndBeginVerification() {
        this._authPrompt.setUser(null);
        this._authPrompt.setQuestion(_('Username'));

        this._showRealmLoginHint(this._realmManager.loginFormat);

        if (this._nextSignalId)
            this._authPrompt.disconnect(this._nextSignalId);
        this._nextSignalId = this._authPrompt.connect('next',
            () => {
                this._authPrompt.disconnect(this._nextSignalId);
                this._nextSignalId = 0;
                this._authPrompt.updateSensitivity(false);
                let answer = this._authPrompt.getAnswer();
                this._user = this._userManager.get_user(answer);
                this._authPrompt.clear();
                this._beginVerification({userName: answer});
            });

        this._sessionMenuButton.updateSensitivity(false);
        this._authPrompt.updateSensitivity(true);
        this._showPrompt();
    }

    _bindOpacity() {
        this._bindings = Main.layoutManager.uiGroup.get_children()
            .filter(c => c !== Main.layoutManager.screenShieldGroup)
            .map(c => this.bind_property('opacity', c, 'opacity', 0));
    }

    _unbindOpacity() {
        this._bindings.forEach(b => b.unbind());
    }

    _loginScreenSessionActivated() {
        if (this.opacity === 255 &&
            this._authPrompt.verificationStatus === AuthPrompt.AuthPromptStatus.NOT_VERIFYING)
            return;

        if (this._authPrompt.verificationStatus !== AuthPrompt.AuthPromptStatus.NOT_VERIFYING)
            this._authPrompt.reset();

        this._bindOpacity();
        this.ease({
            opacity: 255,
            duration: _FADE_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this._unbindOpacity(),
        });
    }

    async _getGreeterSessionProxy() {
        const loginManager = LoginManager.getLoginManager();
        this._greeterSessionProxy = await loginManager.getCurrentSessionProxy();
        this._greeterSessionProxy?.connectObject('g-properties-changed', (proxy, properties) => {
            const activeChanged = !!properties.lookup_value('Active', null);
            if (activeChanged && proxy.Active)
                this._loginScreenSessionActivated();
        }, this);
    }

    _notifyConflictingSessionDialogClosed(userName) {
        const source = new MessageTray.getSystemSource();

        this._conflictingSessionNotification = new MessageTray.Notification({
            source,
            title: _('Stop conflicting session dialog closed'),
            body: _('Try to login again to start a session for user %s.').format(userName),
            urgency: MessageTray.Urgency.CRITICAL,
            isTransient: true,
        });
        this._conflictingSessionNotification.connect('destroy', () => {
            this._conflictingSessionNotification = null;
        });

        source.addNotification(this._conflictingSessionNotification);
    }

    _showConflictingSessionDialog(serviceName, conflictingSession) {
        let conflictingSessionDialog = new ConflictingSessionDialog(conflictingSession,
            this._greeterSessionProxy,
            this._user.get_user_name());

        conflictingSessionDialog.connect('cancel', () => {
            this._authPrompt.reset();
            conflictingSessionDialog.close();
        });
        conflictingSessionDialog.connect('force-stop', () => {
            this._greeter.call_stop_conflicting_session_sync(null);
        });

        const loginManager = LoginManager.getLoginManager();
        loginManager.connectObject('session-removed', (lm, sessionId) => {
            if (sessionId === conflictingSession.Id) {
                conflictingSessionDialog.close();
                this._authPrompt.finish(() => this._startSession(serviceName));
            }
        }, conflictingSessionDialog);

        const closeDialogTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, _CONFLICTING_SESSION_DIALOG_TIMEOUT, () => {
            this._notifyConflictingSessionDialogClosed(this._user.get_user_name());
            conflictingSessionDialog.close();
            this._authPrompt.reset();
            return GLib.SOURCE_REMOVE;
        });

        conflictingSessionDialog.connect('closed', () => {
            GLib.source_remove(closeDialogTimeoutId);
        });

        conflictingSessionDialog.open();
    }

    _startSession(serviceName) {
        this._bindOpacity();
        this.ease({
            opacity: 0,
            duration: _FADE_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this._greeter.call_start_session_when_ready_sync(serviceName, true, null);
                this._unbindOpacity();
            },
        });
    }

    async _findConflictingSession(ignoreSessionId) {
        const userName = this._user.get_user_name();
        const loginManager = LoginManager.getLoginManager();
        const sessions = await loginManager.listSessions();
        for (const session of sessions.map(([id, , user, , path]) => ({id, user, path}))) {
            if (ignoreSessionId === session.id)
                continue;

            if (userName !== session.user)
                continue;

            const sessionProxy = loginManager.getSession(session.path);

            if (sessionProxy.Type !== 'wayland' && sessionProxy.Type !== 'x11')
                continue;

            if (sessionProxy.State !== 'active' && sessionProxy.State !== 'online')
                continue;

            return sessionProxy;
        }

        return null;
    }

    async _onSessionOpened(client, serviceName, sessionId) {
        if (sessionId) {
            const conflictingSession = await this._findConflictingSession(sessionId);
            if (conflictingSession) {
                this._showConflictingSessionDialog(serviceName, conflictingSession);
                return;
            }
        }

        this._authPrompt.finish(() => this._startSession(serviceName));
    }

    _waitForItemForUser(userName) {
        let item = this._userList.getItemFromUserName(userName);

        if (item)
            return null;

        let hold = new Batch.Hold();
        let signalId = this._userList.connect('item-added',
            () => {
                item = this._userList.getItemFromUserName(userName);

                if (item)
                    hold.release();
            });

        hold.connect('release', () => this._userList.disconnect(signalId));

        return hold;
    }

    _blockTimedLoginUntilIdle() {
        let hold = new Batch.Hold();

        this._timedLoginIdleTimeOutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, _TIMED_LOGIN_IDLE_THRESHOLD,
            () => {
                this._timedLoginIdleTimeOutId = 0;
                hold.release();
                return GLib.SOURCE_REMOVE;
            });
        GLib.Source.set_name_by_id(this._timedLoginIdleTimeOutId, '[gnome-shell] this._timedLoginIdleTimeOutId');
        return hold;
    }

    _startTimedLogin(userName, delay) {
        let firstRun = true;

        // Cancel execution of old batch
        if (this._timedLoginBatch) {
            this._timedLoginBatch.cancel();
            this._timedLoginBatch = null;
            firstRun = false;
        }

        // Reset previous idle-timeout
        if (this._timedLoginIdleTimeOutId) {
            GLib.source_remove(this._timedLoginIdleTimeOutId);
            this._timedLoginIdleTimeOutId = 0;
        }

        let loginItem = null;
        let animationTime;

        let tasks = [
            () => {
                if (this._disableUserList)
                    return null;

                this._timedLoginUserListHold = this._waitForItemForUser(userName);
                return this._timedLoginUserListHold;
            },

            () => {
                this._timedLoginUserListHold = null;

                if (this._disableUserList)
                    loginItem = this._authPrompt;
                else
                    loginItem = this._userList.getItemFromUserName(userName);

                // If there is an animation running on the item, reset it.
                loginItem.hideTimedLoginIndicator();
            },

            () => {
                if (this._disableUserList)
                    return;

                // If we're just starting out, start on the right item.
                if (!this._userManager.is_loaded)
                    this._userList.jumpToItem(loginItem);
            },

            () => {
                // This blocks the timed login animation until a few
                // seconds after the user stops interacting with the
                // login screen.

                // We skip this step if the timed login delay is very short.
                if (delay > _TIMED_LOGIN_IDLE_THRESHOLD) {
                    animationTime = delay - _TIMED_LOGIN_IDLE_THRESHOLD;
                    return this._blockTimedLoginUntilIdle();
                } else {
                    animationTime = delay;
                    return null;
                }
            },

            () => {
                if (this._disableUserList)
                    return;

                // If idle timeout is done, make sure the timed login indicator is shown
                if (delay > _TIMED_LOGIN_IDLE_THRESHOLD &&
                    this._authPrompt.visible)
                    this._authPrompt.cancel();

                if (delay > _TIMED_LOGIN_IDLE_THRESHOLD || firstRun) {
                    this._userList.scrollToItem(loginItem);
                    loginItem.grab_key_focus();
                }
            },

            () => loginItem.showTimedLoginIndicator(animationTime),

            () => {
                this._timedLoginBatch = null;
                this._greeter.call_begin_auto_login_sync(userName, null);
            },
        ];

        this._timedLoginBatch = new Batch.ConsecutiveBatch(this, tasks);

        return this._timedLoginBatch.run();
    }

    _onTimedLoginRequested(client, userName, seconds) {
        if (this._timedLoginBatch)
            return;

        this._startTimedLogin(userName, seconds);

        // Restart timed login on user interaction
        global.stage.connect('captured-event', (actor, event) => {
            if (event.type() === Clutter.EventType.KEY_PRESS ||
                event.type() === Clutter.EventType.BUTTON_PRESS)
                this._startTimedLogin(userName, seconds);

            return Clutter.EVENT_PROPAGATE;
        });
    }

    _beginVerification(params) {
        params = Params.parse(params, {
            userName: null,
            hold: null,
        });

        this._authPrompt.begin(params);

        this._loginOptionsButton.updateSensitivity(true);
    }

    _setUserListExpanded(expanded) {
        this._userList.updateStyle(expanded);
        this._userSelectionBox.visible = expanded;
    }

    _hideUserList() {
        this._loginOptionsButton.clearItems();
        this._setUserListExpanded(false);
        if (this._userSelectionBox.visible)
            GdmUtil.cloneAndFadeOutActor(this._userSelectionBox);
    }

    _hideUserListAskForUsernameAndBeginVerification() {
        this._hideUserList();
        this._askForUsernameAndBeginVerification();
    }

    _hideUserListAndBeginVerification() {
        this._hideUserList();
        this._beginVerification();
    }

    _showUserList() {
        this._ensureUserListLoaded();
        this._authPrompt.hide();
        this._hideBannerView();
        this._loginOptionsButton.clearItems();
        this._loginOptionsButton.updateSensitivity(false);
        this._sessionMenuButton.updateSensitivity(false);
        this._setUserListExpanded(true);
        this._notListedButton.show();
        this._userList.grab_key_focus();
    }

    _beginVerificationForItem(item) {
        this._authPrompt.setUser(item.user);

        let userName = item.user.get_user_name();
        let hold = new Batch.Hold();

        this._beginVerification({userName, hold});

        return hold;
    }

    _onUserListActivated(activatedItem) {
        this._user = activatedItem.user;

        this._updateCancelButton();

        if (this._conflictingSessionNotification)
            this._conflictingSessionNotification.destroy();

        const batch = new Batch.ConcurrentBatch(this, [
            GdmUtil.cloneAndFadeOutActor(this._userSelectionBox),
            this._beginVerificationForItem(activatedItem),
        ]);
        batch.run();
    }

    _onDestroy() {
        if (this._settings) {
            this._settings.run_dispose();
            this._settings = null;
        }
        this._greeter = null;
        this._greeterSessionProxy = null;
        this._realmManager?.release();
        this._realmManager = null;
    }

    _loadUserList() {
        if (this._userListLoaded)
            return GLib.SOURCE_REMOVE;

        this._userListLoaded = true;

        let users = this._userManager.list_users();

        for (let i = 0; i < users.length; i++)
            this._userList.addUser(users[i]);

        this._updateDisableUserList();

        this._userManager.connectObject(
            'user-added', (userManager, user) => {
                this._userList.addUser(user);
                this._updateDisableUserList();
            },
            'user-removed', (userManager, user) => {
                this._userList.removeUser(user);
                this._updateDisableUserList();
            },
            'user-changed', (userManager, user) => {
                if (this._userList.containsUser(user) && user.locked)
                    this._userList.removeUser(user);
                else if (!this._userList.containsUser(user) && !user.locked)
                    this._userList.addUser(user);
                this._updateDisableUserList();
            }, this);

        return GLib.SOURCE_REMOVE;
    }

    activate() {
        this._userList.grab_key_focus();
        this.show();
    }

    open() {
        Main.ctrlAltTabManager.addGroup(this,
            _('Login Window'),
            'dialog-password-symbolic',
            {sortGroup: CtrlAltTab.SortGroup.MIDDLE});
        this.activate();

        // Clutter doesn't yet fully support invisible parents with forced
        // visible children and will make everything invisible (flicker) on
        // the first frame if we start at 0. So we start at 1 instead...
        this.opacity = 1;
        this._logoBin.set_opacity_override(255);

        this._grab = Main.pushModal(global.stage, {actionMode: Shell.ActionMode.LOGIN_SCREEN});

        this.ease({
            opacity: 255,
            duration: 1000,
            mode: Clutter.AnimationMode.EASE_IN_QUAD,
            onComplete: () => { this._logoBin.set_opacity_override(-1); },
        });

        return true;
    }

    close() {
        Main.popModal(this._grab);
        this._grab = null;
        Main.ctrlAltTabManager.removeGroup(this);
    }

    cancel() {
        this._authPrompt.cancel();
    }

    addCharacter(_unichar) {
        // Don't allow type ahead at the login screen
    }

    finish(onComplete) {
        this._authPrompt.finish(onComplete);
    }
});
(uuay)appDisplay.js��// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import Graphene from 'gi://Graphene';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as AppFavorites from './appFavorites.js';
import {AppMenu} from './appMenu.js';
import * as BoxPointer from './boxpointer.js';
import * as DND from './dnd.js';
import * as GrabHelper from './grabHelper.js';
import * as IconGrid from './iconGrid.js';
import * as Layout from './layout.js';
import * as PageIndicators from './pageIndicators.js';
import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
import * as PopupMenu from './popupMenu.js';
import * as Search from './search.js';
import * as SwipeTracker from './swipeTracker.js';
import * as SystemActions from '../misc/systemActions.js';

import * as Main from './main.js';

const MENU_POPUP_TIMEOUT = 600;
const POPDOWN_DIALOG_TIMEOUT = 500;

const FOLDER_SUBICON_FRACTION = .4;

const VIEWS_SWITCH_TIME = 400;
const VIEWS_SWITCH_ANIMATION_DELAY = 100;

const SCROLL_TIMEOUT_TIME = 150;

const APP_ICON_SCALE_IN_TIME = 500;
const APP_ICON_SCALE_IN_DELAY = 700;

const APP_ICON_TITLE_EXPAND_TIME = 200;
const APP_ICON_TITLE_COLLAPSE_TIME = 100;

const FOLDER_DIALOG_ANIMATION_TIME = 200;

const PAGE_PREVIEW_ANIMATION_TIME = 150;
const PAGE_INDICATOR_FADE_TIME = 200;
const PAGE_PREVIEW_RATIO = 0.20;

const DRAG_PAGE_SWITCH_INITIAL_TIMEOUT = 1000;
const DRAG_PAGE_SWITCH_IMMEDIATELY_THRESHOLD_PX = 20;

const DRAG_PAGE_SWITCH_REPEAT_TIMEOUT = 1000;

const DELAYED_MOVE_TIMEOUT = 200;

const DIALOG_SHADE_NORMAL = Clutter.Color.from_pixel(0x000000cc);
const DIALOG_SHADE_HIGHLIGHT = Clutter.Color.from_pixel(0x00000055);

const DEFAULT_FOLDERS = {
    'Utilities': {
        name: 'X-GNOME-Utilities.directory',
        categories: ['X-GNOME-Utilities'],
        apps: [
            'gnome-abrt.desktop',
            'gnome-system-log.desktop',
            'nm-connection-editor.desktop',
            'org.gnome.baobab.desktop',
            'org.gnome.Connections.desktop',
            'org.gnome.DejaDup.desktop',
            'org.gnome.Dictionary.desktop',
            'org.gnome.DiskUtility.desktop',
            'org.gnome.Evince.desktop',
            'org.gnome.FileRoller.desktop',
            'org.gnome.fonts.desktop',
            'org.gnome.Loupe.desktop',
            'org.gnome.seahorse.Application.desktop',
            'org.gnome.tweaks.desktop',
            'org.gnome.Usage.desktop',
            'vinagre.desktop',
        ],
    },
    'YaST': {
        name: 'suse-yast.directory',
        categories: ['X-SuSE-YaST'],
    },
    'Pardus': {
        name: 'X-Pardus-Apps.directory',
        categories: ['X-Pardus-Apps'],
    },
};

function _getCategories(info) {
    let categoriesStr = info.get_categories();
    if (!categoriesStr)
        return [];
    return categoriesStr.split(';');
}

function _listsIntersect(a, b) {
    for (let itemA of a) {
        if (b.includes(itemA))
            return true;
    }
    return false;
}

function _getFolderName(folder) {
    let name = folder.get_string('name');

    if (folder.get_boolean('translate')) {
        let translated = Shell.util_get_translated_folder_name(name);
        if (translated !== null)
            return translated;
    }

    return name;
}

function _getViewFromIcon(icon) {
    for (let parent = icon.get_parent(); parent; parent = parent.get_parent()) {
        if (parent instanceof BaseAppView)
            return parent;
    }
    return null;
}

function _findBestFolderName(apps) {
    let appInfos = apps.map(app => app.get_app_info());

    let categoryCounter = {};
    let commonCategories = [];

    appInfos.reduce((categories, appInfo) => {
        for (let category of _getCategories(appInfo)) {
            if (!(category in categoryCounter))
                categoryCounter[category] = 0;

            categoryCounter[category] += 1;

            // If a category is present in all apps, its counter will
            // reach appInfos.length
            if (category.length > 0 &&
                categoryCounter[category] === appInfos.length)
                categories.push(category);
        }
        return categories;
    }, commonCategories);

    for (let category of commonCategories) {
        const directory = `${category}.directory`;
        const translated = Shell.util_get_translated_folder_name(directory);
        if (translated !== null)
            return translated;
    }

    return null;
}

export const AppGrid = GObject.registerClass({
    Properties: {
        'indicators-padding': GObject.ParamSpec.boxed('indicators-padding',
            'Indicators padding', 'Indicators padding',
            GObject.ParamFlags.READWRITE,
            Clutter.Margin.$gtype),
    },
}, class AppGrid extends IconGrid.IconGrid {
    _init(layoutParams) {
        super._init(layoutParams);

        this._indicatorsPadding = new Clutter.Margin();
    }

    _updatePadding() {
        const node = this.get_theme_node();
        const {rowSpacing, columnSpacing} = this.layoutManager;

        const padding = this._indicatorsPadding.copy();
        padding.left += rowSpacing;
        padding.right += rowSpacing;
        padding.top += columnSpacing;
        padding.bottom += columnSpacing;
        ['top', 'right', 'bottom', 'left'].forEach(side => {
            padding[side] += node.get_length(`page-padding-${side}`);
        });

        this.layoutManager.pagePadding = padding;
    }

    vfunc_style_changed() {
        super.vfunc_style_changed();
        this._updatePadding();
    }

    get indicatorsPadding() {
        return this._indicatorsPadding;
    }

    set indicatorsPadding(v) {
        if (this._indicatorsPadding === v)
            return;

        this._indicatorsPadding = v ? v : new Clutter.Margin();
        this._updatePadding();
    }
});

const BaseAppViewGridLayout = GObject.registerClass(
class BaseAppViewGridLayout extends Clutter.BinLayout {
    _init(grid, scrollView, nextPageIndicator, nextPageArrow,
        previousPageIndicator, previousPageArrow) {
        if (!(grid instanceof AppGrid))
            throw new Error('Grid must be an AppGrid subclass');

        super._init();

        this._grid = grid;
        this._scrollView = scrollView;
        this._previousPageIndicator = previousPageIndicator;
        this._previousPageArrow = previousPageArrow;
        this._nextPageIndicator = nextPageIndicator;
        this._nextPageArrow = nextPageArrow;

        grid.connect('pages-changed', () => this._syncPageIndicatorsVisibility());

        this._pageIndicatorsAdjustment = new St.Adjustment({
            lower: 0,
            upper: 1,
        });
        this._pageIndicatorsAdjustment.connect(
            'notify::value', () => this._syncPageIndicators());

        this._showIndicators = false;
        this._currentPage = 0;
        this._pageWidth = 0;
    }

    _getIndicatorsWidth(box) {
        const [width, height] = box.get_size();
        const arrows = [
            this._nextPageArrow,
            this._previousPageArrow,
        ];

        const minArrowsWidth = arrows.reduce(
            (previousWidth, accessory) => {
                const [min] = accessory.get_preferred_width(height);
                return Math.max(previousWidth, min);
            }, 0);

        const idealIndicatorWidth = (width * PAGE_PREVIEW_RATIO) / 2;

        return Math.max(idealIndicatorWidth, minArrowsWidth);
    }

    _syncPageIndicatorsVisibility(animate = true) {
        const previousIndicatorsVisible =
            this._currentPage > 0 && this._showIndicators;

        if (previousIndicatorsVisible)
            this._previousPageIndicator.show();

        this._previousPageIndicator.ease({
            opacity: previousIndicatorsVisible ? 255 : 0,
            duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
            onComplete: () => {
                if (!previousIndicatorsVisible)
                    this._previousPageIndicator.hide();
            },
        });

        const previousArrowVisible =
            this._currentPage > 0 && !previousIndicatorsVisible;

        if (previousArrowVisible)
            this._previousPageArrow.show();

        this._previousPageArrow.ease({
            opacity: previousArrowVisible ? 255 : 0,
            duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
            onComplete: () => {
                if (!previousArrowVisible)
                    this._previousPageArrow.hide();
            },
        });

        // Always show the next page indicator to allow dropping
        // icons into new pages
        const {allowIncompletePages, nPages} = this._grid.layoutManager;
        const nextIndicatorsVisible = this._showIndicators &&
            (allowIncompletePages ? true : this._currentPage < nPages - 1);

        if (nextIndicatorsVisible)
            this._nextPageIndicator.show();

        this._nextPageIndicator.ease({
            opacity: nextIndicatorsVisible ? 255 : 0,
            duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
            onComplete: () => {
                if (!nextIndicatorsVisible)
                    this._nextPageIndicator.hide();
            },
        });

        const nextArrowVisible =
            this._currentPage < nPages - 1 &&
            !nextIndicatorsVisible;

        if (nextArrowVisible)
            this._nextPageArrow.show();

        this._nextPageArrow.ease({
            opacity: nextArrowVisible ? 255 : 0,
            duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
            onComplete: () => {
                if (!nextArrowVisible)
                    this._nextPageArrow.hide();
            },
        });
    }

    _getEndIcon(icons) {
        const {columnsPerPage} = this._grid.layoutManager;
        const index = Math.min(icons.length, columnsPerPage);
        return icons[Math.max(index - 1, 0)];
    }

    _translatePreviousPageIcons(value, ltr) {
        if (this._currentPage === 0)
            return;

        const previousPage = this._currentPage - 1;
        const icons = this._grid.getItemsAtPage(previousPage).filter(i => i.visible);
        if (icons.length === 0)
            return;

        const {left, right} = this._grid.indicatorsPadding;
        const {columnSpacing} = this._grid.layoutManager;
        const endIcon = this._getEndIcon(icons);
        let iconOffset;

        if (ltr) {
            const currentPageOffset = this._pageWidth * this._currentPage;
            iconOffset = currentPageOffset - endIcon.allocation.x2 + left - columnSpacing;
        } else {
            const rtlPage = this._grid.nPages - previousPage - 1;
            const pageOffset = this._pageWidth * rtlPage;
            iconOffset = pageOffset - endIcon.allocation.x1 - right + columnSpacing;
        }

        for (const icon of icons)
            icon.translationX = iconOffset * value;
    }

    _translateNextPageIcons(value, ltr) {
        if (this._currentPage >= this._grid.nPages - 1)
            return;

        const nextPage = this._currentPage + 1;
        const icons = this._grid.getItemsAtPage(nextPage).filter(i => i.visible);
        if (icons.length === 0)
            return;

        const {left, right} = this._grid.indicatorsPadding;
        const {columnSpacing} = this._grid.layoutManager;
        let iconOffset;

        if (ltr) {
            const pageOffset = this._pageWidth * nextPage;
            iconOffset = pageOffset - icons[0].allocation.x1 - right + columnSpacing;
        } else {
            const rtlPage = this._grid.nPages - this._currentPage - 1;
            const currentPageOffset = this._pageWidth * rtlPage;
            iconOffset = currentPageOffset - icons[0].allocation.x2 + left - columnSpacing;
        }

        for (const icon of icons)
            icon.translationX = iconOffset * value;
    }

    _syncPageIndicators() {
        if (!this._container)
            return;

        const {value} = this._pageIndicatorsAdjustment;

        const ltr = this._container.get_text_direction() !== Clutter.TextDirection.RTL;
        const {left, right} = this._grid.indicatorsPadding;
        const leftIndicatorOffset = -left * (1 - value);
        const rightIndicatorOffset = right * (1 - value);

        this._previousPageIndicator.translationX =
            ltr ? leftIndicatorOffset : rightIndicatorOffset;
        this._nextPageIndicator.translationX =
            ltr ? rightIndicatorOffset : leftIndicatorOffset;

        const leftArrowOffset = -left * value;
        const rightArrowOffset = right * value;

        this._previousPageArrow.translationX =
            ltr ? leftArrowOffset : rightArrowOffset;
        this._nextPageArrow.translationX =
            ltr ? rightArrowOffset : leftArrowOffset;

        // Page icons
        this._translatePreviousPageIcons(value, ltr);
        this._translateNextPageIcons(value, ltr);

        if (this._grid.nPages > 0) {
            this._grid.getItemsAtPage(this._currentPage).forEach(icon => {
                icon.translationX = 0;
            });
        }
    }

    vfunc_set_container(container) {
        this._container = container;
        this._pageIndicatorsAdjustment.actor = container;
        this._syncPageIndicators();
    }

    vfunc_allocate(container, box) {
        const ltr = container.get_text_direction() !== Clutter.TextDirection.RTL;
        const indicatorsWidth = this._getIndicatorsWidth(box);

        this._grid.indicatorsPadding = new Clutter.Margin({
            left: indicatorsWidth,
            right: indicatorsWidth,
        });

        this._scrollView.allocate(box);

        const leftBox = box.copy();
        leftBox.x2 = leftBox.x1 + indicatorsWidth;

        const rightBox = box.copy();
        rightBox.x1 = rightBox.x2 - indicatorsWidth;

        this._previousPageIndicator.allocate(ltr ? leftBox : rightBox);
        this._previousPageArrow.allocate_align_fill(ltr ? leftBox : rightBox,
            0.5, 0.5, false, false);
        this._nextPageIndicator.allocate(ltr ? rightBox : leftBox);
        this._nextPageArrow.allocate_align_fill(ltr ? rightBox : leftBox,
            0.5, 0.5, false, false);

        this._pageWidth = box.get_width();
    }

    goToPage(page, animate = true) {
        if (this._currentPage === page)
            return;

        this._currentPage = page;
        this._syncPageIndicatorsVisibility(animate);
        this._syncPageIndicators();
    }

    showPageIndicators() {
        if (this._showIndicators)
            return;

        this._pageIndicatorsAdjustment.ease(1, {
            duration: PAGE_PREVIEW_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
        });

        this._grid.clipToView = false;
        this._showIndicators = true;
        this._syncPageIndicatorsVisibility();
    }

    hidePageIndicators() {
        if (!this._showIndicators)
            return;

        this._pageIndicatorsAdjustment.ease(0, {
            duration: PAGE_PREVIEW_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            onComplete: () => {
                this._grid.clipToView = true;
            },
        });

        this._showIndicators = false;
        this._syncPageIndicatorsVisibility();
    }
});

var BaseAppView = GObject.registerClass({
    GTypeFlags: GObject.TypeFlags.ABSTRACT,
    Properties: {
        'gesture-modes': GObject.ParamSpec.flags(
            'gesture-modes', 'gesture-modes', 'gesture-modes',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            Shell.ActionMode, Shell.ActionMode.OVERVIEW),
    },
    Signals: {
        'view-loaded': {},
    },
}, class BaseAppView extends St.Widget {
    _init(params = {}) {
        super._init(params);

        this._grid = this._createGrid();
        this._grid._delegate = this;
        // Standard hack for ClutterBinLayout
        this._grid.x_expand = true;
        this._grid.connect('pages-changed', () => {
            this.goToPage(this._grid.currentPage);
            this._pageIndicators.setNPages(this._grid.nPages);
            this._pageIndicators.setCurrentPosition(this._grid.currentPage);
        });

        // Scroll View
        this._scrollView = new St.ScrollView({
            style_class: 'apps-scroll-view',
            clip_to_allocation: true,
            x_expand: true,
            y_expand: true,
            reactive: true,
            enable_mouse_scrolling: false,
            hscrollbar_policy: St.PolicyType.EXTERNAL,
            vscrollbar_policy: St.PolicyType.NEVER,
            child: this._grid,
        });

        this._canScroll = true; // limiting scrolling speed
        this._scrollTimeoutId = 0;
        this._scrollView.connect('scroll-event', this._onScroll.bind(this));

        this._adjustment = this._scrollView.hadjustment;
        this._adjustment.connect('notify::value', adj => {
            const value = adj.value / adj.page_size;
            this._pageIndicators.setCurrentPosition(value);
        });

        // Page Indicators
        this._pageIndicators =
            new PageIndicators.PageIndicators(Clutter.Orientation.HORIZONTAL);

        this._pageIndicators.y_expand = false;
        this._pageIndicators.connect('page-activated',
            (indicators, pageIndex) => {
                this.goToPage(pageIndex);
            });
        this._pageIndicators.connect('scroll-event', (actor, event) => {
            this._scrollView.event(event, false);
        });

        // Navigation indicators
        this._nextPageIndicator = new St.Widget({
            style_class: 'page-navigation-hint next',
            opacity: 0,
            visible: false,
            reactive: false,
            x_expand: true,
            y_expand: true,
            x_align: Clutter.ActorAlign.FILL,
            y_align: Clutter.ActorAlign.FILL,
        });

        this._prevPageIndicator = new St.Widget({
            style_class: 'page-navigation-hint previous',
            opacity: 0,
            visible: false,
            reactive: false,
            x_expand: true,
            y_expand: true,
            x_align: Clutter.ActorAlign.FILL,
            y_align: Clutter.ActorAlign.FILL,
        });

        // Next/prev page arrows
        const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
        this._nextPageArrow = new St.Button({
            style_class: 'page-navigation-arrow',
            icon_name: rtl
                ? 'carousel-arrow-previous-symbolic'
                : 'carousel-arrow-next-symbolic',
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._nextPageArrow.connect('clicked',
            () => this.goToPage(this._grid.currentPage + 1));

        this._prevPageArrow = new St.Button({
            style_class: 'page-navigation-arrow',
            icon_name: rtl
                ? 'carousel-arrow-next-symbolic'
                : 'carousel-arrow-previous-symbolic',
            opacity: 0,
            visible: false,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._prevPageArrow.connect('clicked',
            () => this.goToPage(this._grid.currentPage - 1));

        const scrollContainer = new St.Widget({
            clip_to_allocation: true,
            y_expand: true,
        });
        scrollContainer.add_child(this._scrollView);
        scrollContainer.add_child(this._prevPageIndicator);
        scrollContainer.add_child(this._nextPageIndicator);
        scrollContainer.add_child(this._nextPageArrow);
        scrollContainer.add_child(this._prevPageArrow);
        scrollContainer.layoutManager = new BaseAppViewGridLayout(
            this._grid,
            this._scrollView,
            this._nextPageIndicator,
            this._nextPageArrow,
            this._prevPageIndicator,
            this._prevPageArrow);
        this._appGridLayout = scrollContainer.layoutManager;
        scrollContainer._delegate = this;

        this._box = new St.BoxLayout({
            vertical: true,
            x_expand: true,
            y_expand: true,
        });
        this._box.add_child(scrollContainer);
        this._box.add_child(this._pageIndicators);

        // Swipe
        this._swipeTracker = new SwipeTracker.SwipeTracker(this._scrollView,
            Clutter.Orientation.HORIZONTAL, this.gestureModes);
        this._swipeTracker.orientation = Clutter.Orientation.HORIZONTAL;
        this._swipeTracker.connect('begin', this._swipeBegin.bind(this));
        this._swipeTracker.connect('update', this._swipeUpdate.bind(this));
        this._swipeTracker.connect('end', this._swipeEnd.bind(this));

        this._orientation = Clutter.Orientation.HORIZONTAL;

        this._items = new Map();
        this._orderedItems = [];

        // Filter the apps through the user’s parental controls.
        this._parentalControlsManager = ParentalControlsManager.getDefault();
        this._parentalControlsManager.connectObject('app-filter-changed',
            () => this._redisplay(), this);

        // Don't duplicate favorites
        this._appFavorites = AppFavorites.getAppFavorites();
        this._appFavorites.connectObject('changed',
            () => this._redisplay(), this);

        // Drag n' Drop
        this._lastOvershootCoord = -1;
        this._delayedMoveData = null;

        this._dragBeginId = 0;
        this._dragEndId = 0;
        this._dragCancelledId = 0;

        this.connect('destroy', this._onDestroy.bind(this));

        this._previewedPages = new Map();
    }

    _onDestroy() {
        if (this._swipeTracker) {
            this._swipeTracker.destroy();
            delete this._swipeTracker;
        }

        this._removeDelayedMove();
        this._disconnectDnD();
    }

    _createGrid() {
        return new AppGrid({allow_incomplete_pages: true});
    }

    _onScroll(actor, event) {
        if (this._swipeTracker.canHandleScrollEvent(event))
            return Clutter.EVENT_PROPAGATE;

        if (!this._canScroll)
            return Clutter.EVENT_STOP;

        const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
        const vertical = this._orientation === Clutter.Orientation.VERTICAL;

        let nextPage = this._grid.currentPage;
        switch (event.get_scroll_direction()) {
        case Clutter.ScrollDirection.UP:
            nextPage -= 1;
            break;

        case Clutter.ScrollDirection.DOWN:
            nextPage += 1;
            break;

        case Clutter.ScrollDirection.LEFT:
            if (vertical)
                return Clutter.EVENT_STOP;
            nextPage += rtl ? 1 : -1;
            break;

        case Clutter.ScrollDirection.RIGHT:
            if (vertical)
                return Clutter.EVENT_STOP;
            nextPage += rtl ? -1 : 1;
            break;

        default:
            return Clutter.EVENT_STOP;
        }

        this.goToPage(nextPage);

        this._canScroll = false;
        this._scrollTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
            SCROLL_TIMEOUT_TIME, () => {
                this._canScroll = true;
                this._scrollTimeoutId = 0;
                return GLib.SOURCE_REMOVE;
            });

        return Clutter.EVENT_STOP;
    }

    _swipeBegin(tracker, monitor) {
        if (monitor !== Main.layoutManager.primaryIndex)
            return;

        if (this._dragFocus) {
            this._dragFocus.cancelActions();
            this._dragFocus = null;
        }

        const adjustment = this._adjustment;
        adjustment.remove_transition('value');

        const progress = adjustment.value / adjustment.page_size;
        const points = Array.from({length: this._grid.nPages}, (v, i) => i);
        const size = tracker.orientation === Clutter.Orientation.VERTICAL
            ? this._grid.allocation.get_height() : this._grid.allocation.get_width();

        tracker.confirmSwipe(size, points, progress, Math.round(progress));
    }

    _swipeUpdate(tracker, progress) {
        const adjustment = this._adjustment;
        adjustment.value = progress * adjustment.page_size;
    }

    _swipeEnd(tracker, duration, endProgress) {
        const adjustment = this._adjustment;
        const value = endProgress * adjustment.page_size;

        adjustment.ease(value, {
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            duration,
            onComplete: () => this.goToPage(endProgress, false),
        });
    }

    _connectDnD() {
        this._dragBeginId =
            Main.overview.connect('item-drag-begin', this._onDragBegin.bind(this));
        this._dragEndId =
            Main.overview.connect('item-drag-end', this._onDragEnd.bind(this));
        this._dragCancelledId =
            Main.overview.connect('item-drag-cancelled', this._onDragCancelled.bind(this));
    }

    _disconnectDnD() {
        if (this._dragBeginId > 0) {
            Main.overview.disconnect(this._dragBeginId);
            this._dragBeginId = 0;
        }

        if (this._dragEndId > 0) {
            Main.overview.disconnect(this._dragEndId);
            this._dragEndId = 0;
        }

        if (this._dragCancelledId > 0) {
            Main.overview.disconnect(this._dragCancelledId);
            this._dragCancelledId = 0;
        }

        if (this._dragMonitor) {
            DND.removeDragMonitor(this._dragMonitor);
            this._dragMonitor = null;
        }
    }

    _maybeMoveItem(dragEvent) {
        const [success, x, y] =
            this._grid.transform_stage_point(dragEvent.x, dragEvent.y);

        if (!success)
            return;

        const {source} = dragEvent;
        const [page, position, dragLocation] =
            this._getDropTarget(x, y, source);
        const item = position !== -1
            ? this._grid.getItemAt(page, position) : null;


        // Dragging over invalid parts of the grid cancels the timeout
        if (item === source ||
            this._adjustment.get_transition('value') !== null ||
            page !== this._grid.currentPage ||
            dragLocation === IconGrid.DragLocation.INVALID ||
            dragLocation === IconGrid.DragLocation.ON_ICON) {
            this._removeDelayedMove();
            return;
        }

        if (!this._delayedMoveData ||
            this._delayedMoveData.page !== page ||
            this._delayedMoveData.position !== position) {
            // Update the item with a small delay
            this._removeDelayedMove();
            this._delayedMoveData = {
                page,
                position,
                source,
                destroyId: source.connect('destroy', () => this._removeDelayedMove()),
                timeoutId: GLib.timeout_add(GLib.PRIORITY_DEFAULT,
                    DELAYED_MOVE_TIMEOUT, () => {
                        this._moveItem(source, page, position);
                        this._delayedMoveData.timeoutId = 0;
                        this._removeDelayedMove();
                        return GLib.SOURCE_REMOVE;
                    }),
            };
        }
    }

    _removeDelayedMove() {
        if (!this._delayedMoveData)
            return;

        const {source, destroyId, timeoutId} = this._delayedMoveData;

        if (timeoutId > 0)
            GLib.source_remove(timeoutId);

        if (destroyId > 0)
            source.disconnect(destroyId);

        this._delayedMoveData = null;
    }

    _resetDragPageSwitch() {
        if (this._dragPageSwitchInitialTimeoutId) {
            GLib.source_remove(this._dragPageSwitchInitialTimeoutId);
            delete this._dragPageSwitchInitialTimeoutId;
        }

        if (this._dragPageSwitchRepeatTimeoutId) {
            GLib.source_remove(this._dragPageSwitchRepeatTimeoutId);
            delete this._dragPageSwitchRepeatTimeoutId;
        }

        this._lastOvershootCoord = -1;
    }

    _setupDragPageSwitchRepeat(direction) {
        this._resetDragPageSwitch();

        this._dragPageSwitchRepeatTimeoutId =
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, DRAG_PAGE_SWITCH_REPEAT_TIMEOUT, () => {
                this.goToPage(this._grid.currentPage + direction);

                return GLib.SOURCE_CONTINUE;
            });
        GLib.Source.set_name_by_id(this._dragPageSwitchRepeatTimeoutId,
            '[gnome-shell] this._dragPageSwitchRepeatTimeoutId');
    }

    _dragMaybeSwitchPageImmediately(dragEvent) {
        // When there's an adjacent monitor, this gesture conflicts with
        // dragging to the adjacent monitor, so disable in multi-monitor setups
        if (Main.layoutManager.monitors.length > 1)
            return false;

        // Already animating
        if (this._adjustment.get_transition('value') !== null)
            return false;

        const [gridX, gridY] = this.get_transformed_position();
        const [gridWidth, gridHeight] = this.get_transformed_size();

        const vertical = this._orientation === Clutter.Orientation.VERTICAL;
        const gridStart = vertical
            ? gridY + DRAG_PAGE_SWITCH_IMMEDIATELY_THRESHOLD_PX
            : gridX + DRAG_PAGE_SWITCH_IMMEDIATELY_THRESHOLD_PX;
        const gridEnd = vertical
            ? gridY + gridHeight - DRAG_PAGE_SWITCH_IMMEDIATELY_THRESHOLD_PX
            : gridX + gridWidth - DRAG_PAGE_SWITCH_IMMEDIATELY_THRESHOLD_PX;

        const dragCoord = vertical ? dragEvent.y : dragEvent.x;
        if (dragCoord > gridStart && dragCoord < gridEnd) {
            const moveDelta = Math.abs(this._lastOvershootCoord - dragCoord);

            // We moved back out of the overshoot region into the grid. If the
            // move was sufficiently large, reset the overshoot so that it's
            // possible to repeatedly bump against the edge and quickly switch
            // pages.
            if (this._lastOvershootCoord >= 0 &&
                moveDelta > DRAG_PAGE_SWITCH_IMMEDIATELY_THRESHOLD_PX)
                this._resetDragPageSwitch();

            return false;
        }

        // Still in the area of the previous overshoot
        if (this._lastOvershootCoord >= 0)
            return false;

        let direction = dragCoord <= gridStart ? -1 : 1;
        if (this.get_text_direction() === Clutter.TextDirection.RTL)
            direction *= -1;

        this.goToPage(this._grid.currentPage + direction);
        this._setupDragPageSwitchRepeat(direction);

        this._lastOvershootCoord = dragCoord;

        return true;
    }

    _maybeSetupDragPageSwitchInitialTimeout(dragEvent) {
        if (this._dragPageSwitchInitialTimeoutId || this._dragPageSwitchRepeatTimeoutId)
            return;

        const {targetActor} = dragEvent;

        this._dragPageSwitchInitialTimeoutId =
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, DRAG_PAGE_SWITCH_INITIAL_TIMEOUT, () => {
                const direction = targetActor === this._prevPageIndicator ? -1 : 1;

                this.goToPage(this._grid.currentPage + direction);
                this._setupDragPageSwitchRepeat(direction);

                delete this._dragPageSwitchInitialTimeoutId;
                return GLib.SOURCE_REMOVE;
            });
    }

    _onDragBegin() {
        this._dragMonitor = {
            dragMotion: this._onDragMotion.bind(this),
            dragDrop: this._onDragDrop.bind(this),
        };
        DND.addDragMonitor(this._dragMonitor);
        this._appGridLayout.showPageIndicators();
        this._dragFocus = null;
        this._swipeTracker.enabled = false;
    }

    _onDragMotion(dragEvent) {
        if (!(dragEvent.source instanceof AppViewItem))
            return DND.DragMotionResult.CONTINUE;

        const appIcon = dragEvent.source;

        if (appIcon instanceof AppViewItem) {
            // Two ways of switching pages during DND:
            // 1) When "bumping" the cursor against the monitor edge, we switch
            //    page immediately.
            // 2) When hovering over the next-page indicator for a certain time,
            //    we also switch page.

            if (!this._dragMaybeSwitchPageImmediately(dragEvent)) {
                const {targetActor} = dragEvent;

                if (targetActor === this._prevPageIndicator ||
                    targetActor === this._nextPageIndicator)
                    this._maybeSetupDragPageSwitchInitialTimeout(dragEvent);
                else
                    this._resetDragPageSwitch();
            }
        }

        this._maybeMoveItem(dragEvent);

        return DND.DragMotionResult.CONTINUE;
    }

    _onDragDrop(dropEvent) {
        // Because acceptDrop() does not receive the target actor, store it
        // here and use this value in the acceptDrop() implementation below.
        this._dropTarget = dropEvent.targetActor;
        return DND.DragMotionResult.CONTINUE;
    }

    _onDragEnd() {
        if (this._dragMonitor) {
            DND.removeDragMonitor(this._dragMonitor);
            this._dragMonitor = null;
        }

        this._resetDragPageSwitch();

        this._appGridLayout.hidePageIndicators();
        this._swipeTracker.enabled = true;
    }

    _onDragCancelled() {
        // At this point, the positions aren't stored yet, thus _redisplay()
        // will move all items to their original positions
        this._redisplay();
        this._appGridLayout.hidePageIndicators();
        this._swipeTracker.enabled = true;
    }

    _canAccept(source) {
        return source instanceof AppViewItem;
    }

    handleDragOver(source) {
        if (!this._canAccept(source))
            return DND.DragMotionResult.NO_DROP;

        return DND.DragMotionResult.MOVE_DROP;
    }

    acceptDrop(source) {
        const dropTarget = this._dropTarget;
        delete this._dropTarget;

        if (!this._canAccept(source))
            return false;

        if (dropTarget === this._prevPageIndicator ||
            dropTarget === this._nextPageIndicator) {
            const increment = dropTarget === this._prevPageIndicator ? -1 : 1;
            const {currentPage, nPages} = this._grid;
            const page = Math.min(currentPage + increment, nPages);
            const position = page < nPages ? -1 : 0;

            this._moveItem(source, page, position);
            this.goToPage(page);
        } else if (this._delayedMoveData) {
            // Dropped before the icon was moved
            const {page, position} = this._delayedMoveData;

            this._moveItem(source, page, position);
            this._removeDelayedMove();
        }

        return true;
    }

    _findBestPageToAppend(startPage = 1) {
        for (let i = startPage; i < this._grid.nPages; i++) {
            const pageItems =
                this._grid.getItemsAtPage(i).filter(c => c.visible);

            if (pageItems.length < this._grid.itemsPerPage)
                return i;
        }

        return -1;
    }

    _getLinearPosition(item) {
        const [page, position] = this._grid.getItemPosition(item);
        if (page === -1 || position === -1)
            throw new Error('Item not found in grid');

        let itemIndex = 0;

        if (this._grid.nPages > 0) {
            for (let i = 0; i < page; i++)
                itemIndex += this._grid.getItemsAtPage(i).filter(c => c.visible).length;

            itemIndex += position;
        }

        return itemIndex;
    }

    _addItem(item, page, position) {
        this._items.set(item.id, item);
        this._grid.addItem(item, page, position);

        this._orderedItems.splice(this._getLinearPosition(item), 0, item);
    }

    _removeItem(item) {
        const iconIndex = this._orderedItems.indexOf(item);

        this._orderedItems.splice(iconIndex, 1);
        this._items.delete(item.id);
        this._grid.removeItem(item);
    }

    _getItemPosition(item) {
        const {itemsPerPage} = this._grid;

        let iconIndex = this._orderedItems.indexOf(item);
        if (iconIndex === -1)
            iconIndex = this._orderedItems.length - 1;

        const page = Math.floor(iconIndex / itemsPerPage);
        const position = iconIndex % itemsPerPage;

        return [page, position];
    }

    _redisplay() {
        let oldApps = this._orderedItems.slice();
        let oldAppIds = oldApps.map(icon => icon.id);

        let newApps = this._loadApps().sort(this._compareItems.bind(this));
        let newAppIds = newApps.map(icon => icon.id);

        let addedApps = newApps.filter(icon => !oldAppIds.includes(icon.id));
        let removedApps = oldApps.filter(icon => !newAppIds.includes(icon.id));

        // Remove old app icons
        removedApps.forEach(icon => {
            this._removeItem(icon);
            icon.destroy();
        });

        // Add new app icons, or move existing ones
        newApps.forEach(icon => {
            const [page, position] = this._getItemPosition(icon);
            if (addedApps.includes(icon)) {
                // If there's two pages, newly installed apps should not appear
                // on page 0
                if (page === -1 && position === -1 && this._grid.nPages > 1)
                    this._addItem(icon, 1, -1);
                else
                    this._addItem(icon, page, position);
            } else if (page !== -1 && position !== -1) {
                this._moveItem(icon, page, position);
            } else {
                // App is part of a folder
            }
        });

        this.emit('view-loaded');
    }

    getAllItems() {
        return this._orderedItems;
    }

    _compareItems(a, b) {
        return a.name.localeCompare(b.name);
    }

    _selectAppInternal(id) {
        if (this._items.has(id))
            this._items.get(id).navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
        else
            log(`No such app ${id}`);
    }

    selectApp(id) {
        if (this._items.has(id)) {
            let item = this._items.get(id);

            if (item.mapped) {
                this._selectAppInternal(id);
            } else {
                // Need to wait until the view is mapped
                let signalId = item.connect('notify::mapped', actor => {
                    if (actor.mapped) {
                        actor.disconnect(signalId);
                        this._selectAppInternal(id);
                    }
                });
            }
        } else {
            // Need to wait until the view is built
            let signalId = this.connect('view-loaded', () => {
                this.disconnect(signalId);
                this.selectApp(id);
            });
        }
    }

    _getDropTarget(x, y, source) {
        const [sourcePage, sourcePosition] = this._grid.getItemPosition(source);
        let [targetPage, targetPosition, dragLocation] = this._grid.getDropTarget(x, y);

        let reflowDirection = Clutter.ActorAlign.END;

        if (sourcePosition === targetPosition)
            reflowDirection = -1;

        if (sourcePage === targetPage && sourcePosition < targetPosition)
            reflowDirection = Clutter.ActorAlign.START;
        if (!this._grid.layout_manager.allow_incomplete_pages && sourcePage < targetPage)
            reflowDirection = Clutter.ActorAlign.START;

        // In case we're hovering over the edge of an item but the
        // reflow will happen in the opposite direction (the drag
        // can't "naturally push the item away"), we instead set the
        // drop target to the adjacent item that can be pushed away
        // in the reflow-direction.
        //
        // We must avoid doing that if we're hovering over the first
        // or last column though, in that case there is no adjacent
        // icon we could push away.
        if (dragLocation === IconGrid.DragLocation.START_EDGE &&
            reflowDirection === Clutter.ActorAlign.START) {
            const nColumns = this._grid.layout_manager.columns_per_page;
            const targetColumn = targetPosition % nColumns;

            if (targetColumn > 0) {
                targetPosition -= 1;
                dragLocation = IconGrid.DragLocation.END_EDGE;
            }
        } else if (dragLocation === IconGrid.DragLocation.END_EDGE &&
                   reflowDirection === Clutter.ActorAlign.END) {
            const nColumns = this._grid.layout_manager.columns_per_page;
            const targetColumn = targetPosition % nColumns;

            if (targetColumn < nColumns - 1) {
                targetPosition += 1;
                dragLocation = IconGrid.DragLocation.START_EDGE;
            }
        }

        return [targetPage, targetPosition, dragLocation];
    }

    _moveItem(item, newPage, newPosition) {
        this._grid.moveItem(item, newPage, newPosition);

        // Update the _orderedItems array
        this._orderedItems.splice(this._orderedItems.indexOf(item), 1);
        this._orderedItems.splice(this._getLinearPosition(item), 0, item);
    }

    vfunc_map() {
        this._swipeTracker.enabled = true;
        this._connectDnD();
        super.vfunc_map();
    }

    vfunc_unmap() {
        if (this._swipeTracker)
            this._swipeTracker.enabled = false;
        this._disconnectDnD();
        super.vfunc_unmap();
    }

    animateSwitch(animationDirection) {
        this.remove_all_transitions();
        this._grid.remove_all_transitions();

        let params = {
            duration: VIEWS_SWITCH_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        };
        if (animationDirection === IconGrid.AnimationDirection.IN) {
            this.show();
            params.opacity = 255;
            params.delay = VIEWS_SWITCH_ANIMATION_DELAY;
        } else {
            params.opacity = 0;
            params.delay = 0;
            params.onComplete = () => this.hide();
        }

        this._grid.ease(params);
    }

    goToPage(pageNumber, animate = true) {
        pageNumber = Math.clamp(pageNumber, 0, Math.max(this._grid.nPages - 1, 0));

        if (this._grid.currentPage === pageNumber)
            return;

        this._appGridLayout.goToPage(pageNumber, animate);
        this._grid.goToPage(pageNumber, animate);
    }

    updateDragFocus(dragFocus) {
        this._dragFocus = dragFocus;
    }
});

const PageManager = GObject.registerClass({
    Signals: {'layout-changed': {}},
}, class PageManager extends GObject.Object {
    _init() {
        super._init();

        this._updatingPages = false;
        this._loadPages();

        global.settings.connect('changed::app-picker-layout',
            this._loadPages.bind(this));
    }

    _loadPages() {
        const layout = global.settings.get_value('app-picker-layout');
        this._pages = layout.recursiveUnpack();
        if (!this._updatingPages)
            this.emit('layout-changed');
    }

    getAppPosition(appId) {
        let position = -1;
        let page = -1;

        for (let pageIndex = 0; pageIndex < this._pages.length; pageIndex++) {
            const pageData = this._pages[pageIndex];

            if (appId in pageData) {
                page = pageIndex;
                position = pageData[appId].position;
                break;
            }
        }

        return [page, position];
    }

    set pages(p) {
        const packedPages = [];

        // Pack the icon properties as a GVariant
        for (const page of p) {
            const pageData = {};
            for (const [appId, properties] of Object.entries(page))
                pageData[appId] = new GLib.Variant('a{sv}', properties);
            packedPages.push(pageData);
        }

        this._updatingPages = true;

        const variant = new GLib.Variant('aa{sv}', packedPages);
        global.settings.set_value('app-picker-layout', variant);

        this._updatingPages = false;
    }

    get pages() {
        return this._pages;
    }
});

export const AppDisplay = GObject.registerClass(
class AppDisplay extends BaseAppView {
    _init() {
        super._init({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
        });

        this._pageManager = new PageManager();
        this._pageManager.connect('layout-changed', () => this._redisplay());

        this.add_child(this._box);

        this._folderIcons = [];

        this._currentDialog = null;
        this._displayingDialog = false;

        this._placeholder = null;

        this._overviewHiddenId = 0;
        this._redisplayWorkId = Main.initializeDeferredWork(this, () => {
            this._redisplay();
            if (this._overviewHiddenId === 0)
                this._overviewHiddenId = Main.overview.connect('hidden', () => this.goToPage(0));
        });

        Shell.AppSystem.get_default().connect('installed-changed', () => {
            Main.queueDeferredWork(this._redisplayWorkId);
        });
        this._folderSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.app-folders'});
        this._ensureDefaultFolders();
        this._folderSettings.connect('changed::folder-children', () => {
            Main.queueDeferredWork(this._redisplayWorkId);
        });
    }

    _onDestroy() {
        super._onDestroy();

        if (this._scrollTimeoutId !== 0) {
            GLib.source_remove(this._scrollTimeoutId);
            this._scrollTimeoutId = 0;
        }
    }

    vfunc_map() {
        this._keyPressEventId =
            global.stage.connect('key-press-event',
                this._onKeyPressEvent.bind(this));
        super.vfunc_map();
    }

    vfunc_unmap() {
        if (this._keyPressEventId) {
            global.stage.disconnect(this._keyPressEventId);
            this._keyPressEventId = 0;
        }
        super.vfunc_unmap();
    }

    _redisplay() {
        this._folderIcons.forEach(icon => {
            icon.view._redisplay();
        });

        super._redisplay();
    }

    _savePages() {
        const pages = [];

        for (let i = 0; i < this._grid.nPages; i++) {
            const pageItems =
                this._grid.getItemsAtPage(i).filter(c => c.visible);
            const pageData = {};

            pageItems.forEach((item, index) => {
                pageData[item.id] = {
                    position: GLib.Variant.new_int32(index),
                };
            });
            pages.push(pageData);
        }

        this._pageManager.pages = pages;
    }

    _ensureDefaultFolders() {
        if (this._folderSettings.get_strv('folder-children').length > 0)
            return;

        const folders = Object.keys(DEFAULT_FOLDERS);
        this._folderSettings.set_strv('folder-children', folders);

        const {path} = this._folderSettings;
        for (const folder of folders) {
            const {name, categories, apps} = DEFAULT_FOLDERS[folder];
            const child = new Gio.Settings({
                schema_id: 'org.gnome.desktop.app-folders.folder',
                path: `${path}folders/${folder}/`,
            });
            child.set_string('name', name);
            child.set_boolean('translate', true);
            child.set_strv('categories', categories);
            if (apps)
                child.set_strv('apps', apps);
        }
    }

    _ensurePlaceholder(source) {
        if (this._placeholder)
            return;

        const appSys = Shell.AppSystem.get_default();
        const app = appSys.lookup_app(source.id);

        const isDraggable =
            global.settings.is_writable('favorite-apps') ||
            global.settings.is_writable('app-picker-layout');

        this._placeholder = new AppIcon(app, {isDraggable});
        this._placeholder.connect('notify::pressed', icon => {
            if (icon.pressed)
                this.updateDragFocus(icon);
        });
        this._placeholder.scaleAndFade();
        this._redisplay();
    }

    _removePlaceholder() {
        if (this._placeholder) {
            this._placeholder.undoScaleAndFade();
            this._placeholder = null;
            this._redisplay();
        }
    }

    getAppInfos() {
        return this._appInfoList;
    }

    _getItemPosition(item) {
        if (item === this._placeholder) {
            let [page, position] = this._grid.getItemPosition(item);

            if (page === -1)
                page = this._grid.currentPage;

            return [page, position];
        }

        return this._pageManager.getAppPosition(item.id);
    }

    _compareItems(a, b) {
        const [aPage, aPosition] = this._getItemPosition(a);
        const [bPage, bPosition] = this._getItemPosition(b);

        if (aPage === -1 && bPage === -1)
            return a.name.localeCompare(b.name);
        else if (aPage === -1)
            return 1;
        else if (bPage === -1)
            return -1;

        if (aPage !== bPage)
            return aPage - bPage;

        return aPosition - bPosition;
    }

    _loadApps() {
        let appIcons = [];
        this._appInfoList = Shell.AppSystem.get_default().get_installed().filter(appInfo => {
            try {
                appInfo.get_id(); // catch invalid file encodings
            } catch (e) {
                return false;
            }
            return !this._appFavorites.isFavorite(appInfo.get_id()) &&
                this._parentalControlsManager.shouldShowApp(appInfo);
        });

        let apps = this._appInfoList.map(app => app.get_id());

        let appSys = Shell.AppSystem.get_default();

        const appsInsideFolders = new Set();
        this._folderIcons = [];

        let folders = this._folderSettings.get_strv('folder-children');
        folders.forEach(id => {
            let path = `${this._folderSettings.path}folders/${id}/`;
            let icon = this._items.get(id);
            if (!icon) {
                icon = new FolderIcon(id, path, this);
                icon.connect('apps-changed', () => {
                    this._redisplay();
                    this._savePages();
                });
                icon.connect('notify::pressed', () => {
                    if (icon.pressed)
                        this.updateDragFocus(icon);
                });
            }

            // Don't try to display empty folders
            if (!icon.visible) {
                icon.destroy();
                return;
            }

            appIcons.push(icon);
            this._folderIcons.push(icon);

            icon.getAppIds().forEach(appId => appsInsideFolders.add(appId));
        });

        // Allow dragging of the icon only if the Dash would accept a drop to
        // change favorite-apps. There are no other possible drop targets from
        // the app picker, so there's no other need for a drag to start,
        // at least on single-monitor setups.
        // This also disables drag-to-launch on multi-monitor setups,
        // but we hope that is not used much.
        const isDraggable =
            global.settings.is_writable('favorite-apps') ||
            global.settings.is_writable('app-picker-layout');

        apps.forEach(appId => {
            if (appsInsideFolders.has(appId))
                return;

            let icon = this._items.get(appId);
            if (!icon) {
                let app = appSys.lookup_app(appId);

                icon = new AppIcon(app, {isDraggable});
                icon.connect('notify::pressed', () => {
                    if (icon.pressed)
                        this.updateDragFocus(icon);
                });
            }

            appIcons.push(icon);
        });

        // At last, if there's a placeholder available, add it
        if (this._placeholder)
            appIcons.push(this._placeholder);

        return appIcons;
    }

    animateSwitch(animationDirection) {
        super.animateSwitch(animationDirection);

        if (this._currentDialog && this._displayingDialog &&
            animationDirection === IconGrid.AnimationDirection.OUT) {
            this._currentDialog.ease({
                opacity: 0,
                duration: VIEWS_SWITCH_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => (this.opacity = 255),
            });
        }
    }

    goToPage(pageNumber, animate = true) {
        pageNumber = Math.clamp(pageNumber, 0, Math.max(this._grid.nPages - 1, 0));

        if (this._grid.currentPage === pageNumber &&
            this._displayingDialog &&
            this._currentDialog)
            return;
        if (this._displayingDialog && this._currentDialog)
            this._currentDialog.popdown();

        super.goToPage(pageNumber, animate);
    }

    _onScroll(actor, event) {
        if (this._displayingDialog || !this._scrollView.reactive)
            return Clutter.EVENT_STOP;

        return super._onScroll(actor, event);
    }

    _onKeyPressEvent(actor, event) {
        if (this._displayingDialog)
            return Clutter.EVENT_STOP;

        if (event.get_key_symbol() === Clutter.KEY_Page_Up) {
            this.goToPage(this._grid.currentPage - 1);
            return Clutter.EVENT_STOP;
        } else if (event.get_key_symbol() === Clutter.KEY_Page_Down) {
            this.goToPage(this._grid.currentPage + 1);
            return Clutter.EVENT_STOP;
        } else if (event.get_key_symbol() === Clutter.KEY_Home) {
            this.goToPage(0);
            return Clutter.EVENT_STOP;
        } else if (event.get_key_symbol() === Clutter.KEY_End) {
            this.goToPage(this._grid.nPages - 1);
            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    addFolderDialog(dialog) {
        Main.layoutManager.overviewGroup.add_child(dialog);
        dialog.connect('open-state-changed', (o, isOpen) => {
            this._currentDialog?.disconnectObject(this);

            this._currentDialog = null;

            if (isOpen) {
                this._currentDialog = dialog;
                this._currentDialog.connectObject('destroy',
                    () => (this._currentDialog = null), this);
            }
            this._displayingDialog = isOpen;
        });
    }

    _maybeMoveItem(dragEvent) {
        const clonedEvent = {
            ...dragEvent,
            source: this._placeholder ? this._placeholder : dragEvent.source,
        };

        super._maybeMoveItem(clonedEvent);
    }

    _onDragBegin(overview, source) {
        super._onDragBegin(overview, source);

        // When dragging from a folder dialog, the dragged app icon doesn't
        // exist in AppDisplay. We work around that by adding a placeholder
        // icon that is either destroyed on cancel, or becomes the effective
        // new icon when dropped.
        if (_getViewFromIcon(source) instanceof FolderView ||
            this._appFavorites.isFavorite(source.id))
            this._ensurePlaceholder(source);
    }

    _onDragMotion(dragEvent) {
        if (this._currentDialog)
            return DND.DragMotionResult.CONTINUE;

        return super._onDragMotion(dragEvent);
    }

    _onDragEnd() {
        super._onDragEnd();
        this._removePlaceholder();
        this._savePages();
    }

    _onDragCancelled(overview, source) {
        const view = _getViewFromIcon(source);

        if (view instanceof FolderView)
            return;

        super._onDragCancelled(overview, source);
    }

    acceptDrop(source) {
        if (!super.acceptDrop(source))
            return false;

        this._savePages();

        let view = _getViewFromIcon(source);
        if (view instanceof FolderView)
            view.removeApp(source.app);

        if (this._currentDialog)
            this._currentDialog.popdown();

        if (this._appFavorites.isFavorite(source.id))
            this._appFavorites.removeFavorite(source.id);

        return true;
    }

    createFolder(apps) {
        let newFolderId = GLib.uuid_string_random();

        let folders = this._folderSettings.get_strv('folder-children');
        folders.push(newFolderId);
        this._folderSettings.set_strv('folder-children', folders);

        // Create the new folder
        let newFolderPath = this._folderSettings.path.concat('folders/', newFolderId, '/');
        let newFolderSettings;
        try {
            newFolderSettings = new Gio.Settings({
                schema_id: 'org.gnome.desktop.app-folders.folder',
                path: newFolderPath,
            });
        } catch (e) {
            log('Error creating new folder');
            return false;
        }

        // The hovered AppIcon always passes its own id as the first
        // one, and this is where we want the folder to be created
        let [folderPage, folderPosition] =
            this._grid.getItemPosition(this._items.get(apps[0]));

        // Adjust the final position
        folderPosition -= apps.reduce((counter, appId) => {
            const [page, position] =
                this._grid.getItemPosition(this._items.get(appId));
            if (page === folderPage && position < folderPosition)
                counter++;
            return counter;
        }, 0);

        let appItems = apps.map(id => this._items.get(id).app);
        let folderName = _findBestFolderName(appItems);
        if (!folderName)
            folderName = _('Unnamed Folder');

        newFolderSettings.delay();
        newFolderSettings.set_string('name', folderName);
        newFolderSettings.set_strv('apps', apps);
        newFolderSettings.apply();

        this._redisplay();

        // Move the folder to where the icon target icon was
        const folderItem = this._items.get(newFolderId);
        this._moveItem(folderItem, folderPage, folderPosition);
        this._savePages();

        return true;
    }
});

export class AppSearchProvider {
    constructor() {
        this._appSys = Shell.AppSystem.get_default();
        this.id = 'applications';
        this.isRemoteProvider = false;
        this.canLaunchSearch = false;
        this.maxResults = 6;

        this._systemActions = new SystemActions.getDefault();

        this._parentalControlsManager = ParentalControlsManager.getDefault();
    }

    getResultMetas(apps) {
        const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
        let metas = [];
        for (let id of apps) {
            if (id.endsWith('.desktop')) {
                let app = this._appSys.lookup_app(id);

                metas.push({
                    id: app.get_id(),
                    name: app.get_name(),
                    createIcon: size => app.create_icon_texture(size),
                });
            } else {
                let name = this._systemActions.getName(id);
                let iconName = this._systemActions.getIconName(id);

                const createIcon = size => new St.Icon({
                    icon_name: iconName,
                    width: size * scaleFactor,
                    height: size * scaleFactor,
                    style_class: 'system-action-icon',
                });

                metas.push({id, name, createIcon});
            }
        }

        return new Promise(resolve => resolve(metas));
    }

    filterResults(results, maxNumber) {
        return results.slice(0, maxNumber);
    }

    getInitialResultSet(terms, cancellable) {
        // Defer until the parental controls manager is initialised, so the
        // results can be filtered correctly.
        if (!this._parentalControlsManager.initialized) {
            return new Promise(resolve => {
                let initializedId = this._parentalControlsManager.connect('app-filter-changed', async () => {
                    if (this._parentalControlsManager.initialized) {
                        this._parentalControlsManager.disconnect(initializedId);
                        resolve(await this.getInitialResultSet(terms, cancellable));
                    }
                });
            });
        }

        let query = terms.join(' ');
        let groups = Shell.AppSystem.search(query);
        let usage = Shell.AppUsage.get_default();
        let results = [];

        groups.forEach(group => {
            group = group.filter(appID => {
                const app = this._appSys.lookup_app(appID);
                return app && this._parentalControlsManager.shouldShowApp(app.app_info);
            });
            results = results.concat(group.sort(
                (a, b) => usage.compare(a, b)));
        });

        results = results.concat(this._systemActions.getMatchingActions(terms));
        return new Promise(resolve => resolve(results));
    }

    getSubsearchResultSet(previousResults, terms, cancellable) {
        return this.getInitialResultSet(terms, cancellable);
    }

    createResultObject(resultMeta) {
        if (resultMeta.id.endsWith('.desktop')) {
            return new AppIcon(this._appSys.lookup_app(resultMeta['id']), {
                expandTitleOnHover: false,
            });
        } else {
            return new SystemActionIcon(this, resultMeta);
        }
    }
}

export const AppViewItem = GObject.registerClass(
class AppViewItem extends St.Button {
    _init(params = {}, isDraggable = true, expandTitleOnHover = true) {
        super._init({
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
            reactive: true,
            button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO,
            can_focus: true,
            ...params,
        });

        this._delegate = this;

        if (isDraggable) {
            this._draggable = DND.makeDraggable(this, {timeoutThreshold: 200});

            this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
            this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
            this._draggable.connect('drag-end', this._onDragEnd.bind(this));
        }

        this._otherIconIsHovering = false;
        this._expandTitleOnHover = expandTitleOnHover;

        if (expandTitleOnHover)
            this.connect('notify::hover', this._onHover.bind(this));
        this.connect('destroy', this._onDestroy.bind(this));
    }

    _onDestroy() {
        if (this._dragMonitor) {
            DND.removeDragMonitor(this._dragMonitor);
            this._dragMonitor = null;
        }

        if (this._draggable) {
            if (this._dragging)
                Main.overview.endItemDrag(this);
            this._draggable = null;
        }
    }

    _updateMultiline() {
        if (!this._expandTitleOnHover || !this.icon.label)
            return;

        const {label} = this.icon;
        const {clutterText} = label;
        const layout = clutterText.get_layout();
        if (!layout.is_wrapped() && !layout.is_ellipsized())
            return;

        label.remove_transition('allocation');

        const id = label.connect('notify::allocation', () => {
            label.restore_easing_state();
            label.disconnect(id);
        });

        const expand = this._forcedHighlight || this.hover || this.has_key_focus();
        label.save_easing_state();
        label.set_easing_duration(expand
            ? APP_ICON_TITLE_EXPAND_TIME
            : APP_ICON_TITLE_COLLAPSE_TIME);
        clutterText.set({
            line_wrap: expand,
            line_wrap_mode: expand ? Pango.WrapMode.WORD_CHAR : Pango.WrapMode.NONE,
            ellipsize: expand ? Pango.EllipsizeMode.NONE : Pango.EllipsizeMode.END,
        });
    }

    _onHover() {
        this._updateMultiline();
    }

    _onDragBegin() {
        this._dragging = true;
        this.scaleAndFade();
        Main.overview.beginItemDrag(this);
    }

    _onDragCancelled() {
        this._dragging = false;
        Main.overview.cancelledItemDrag(this);
    }

    _onDragEnd() {
        this._dragging = false;
        this.undoScaleAndFade();
        Main.overview.endItemDrag(this);
    }

    scaleIn() {
        this.scale_x = 0;
        this.scale_y = 0;

        this.ease({
            scale_x: 1,
            scale_y: 1,
            duration: APP_ICON_SCALE_IN_TIME,
            delay: APP_ICON_SCALE_IN_DELAY,
            mode: Clutter.AnimationMode.EASE_OUT_QUINT,
        });
    }

    scaleAndFade() {
        this.reactive = false;
        this.ease({
            scale_x: 0.5,
            scale_y: 0.5,
            opacity: 0,
        });
    }

    undoScaleAndFade() {
        this.reactive = true;
        this.ease({
            scale_x: 1.0,
            scale_y: 1.0,
            opacity: 255,
        });
    }

    _canAccept(source) {
        return source !== this;
    }

    _setHoveringByDnd(hovering) {
        if (this._otherIconIsHovering === hovering)
            return;

        this._otherIconIsHovering = hovering;

        if (hovering) {
            this._dragMonitor = {
                dragMotion: this._onDragMotion.bind(this),
            };
            DND.addDragMonitor(this._dragMonitor);
        } else {
            DND.removeDragMonitor(this._dragMonitor);
        }
    }

    _onDragMotion(dragEvent) {
        if (!this.contains(dragEvent.targetActor))
            this._setHoveringByDnd(false);

        return DND.DragMotionResult.CONTINUE;
    }

    _withinLeeways(x) {
        return x < IconGrid.LEFT_DIVIDER_LEEWAY ||
            x > this.width - IconGrid.RIGHT_DIVIDER_LEEWAY;
    }

    vfunc_key_focus_in() {
        this._updateMultiline();
        super.vfunc_key_focus_in();
    }

    vfunc_key_focus_out() {
        this._updateMultiline();
        super.vfunc_key_focus_out();
    }

    handleDragOver(source, _actor, x) {
        if (source === this)
            return DND.DragMotionResult.NO_DROP;

        if (!this._canAccept(source))
            return DND.DragMotionResult.CONTINUE;

        if (this._withinLeeways(x)) {
            this._setHoveringByDnd(false);
            return DND.DragMotionResult.CONTINUE;
        }

        this._setHoveringByDnd(true);

        return DND.DragMotionResult.MOVE_DROP;
    }

    acceptDrop(source, _actor, x) {
        this._setHoveringByDnd(false);

        if (!this._canAccept(source))
            return false;

        if (this._withinLeeways(x))
            return false;

        return true;
    }

    cancelActions() {
        if (this._draggable)
            this._draggable.fakeRelease();
        this.fake_release();
    }

    get id() {
        return this._id;
    }

    get name() {
        return this._name;
    }

    setForcedHighlight(highlighted) {
        this._forcedHighlight = highlighted;
        this.set({
            track_hover: !highlighted,
            hover: highlighted,
        });
    }
});

const FolderGrid = GObject.registerClass(
class FolderGrid extends AppGrid {
    _init() {
        super._init({
            allow_incomplete_pages: false,
            columns_per_page: 3,
            rows_per_page: 3,
            page_halign: Clutter.ActorAlign.CENTER,
            page_valign: Clutter.ActorAlign.CENTER,
        });

        this.setGridModes([
            {
                rows: 3,
                columns: 3,
            },
        ]);
    }
});

export const FolderView = GObject.registerClass(
class FolderView extends BaseAppView {
    _init(folder, id, parentView) {
        super._init({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
            gesture_modes: Shell.ActionMode.POPUP,
        });

        // If it not expand, the parent doesn't take into account its preferred_width when allocating
        // the second time it allocates, so we apply the "Standard hack for ClutterBinLayout"
        this._grid.x_expand = true;
        this._id = id;
        this._folder = folder;
        this._parentView = parentView;
        this._grid._delegate = this;

        this.add_child(this._box);

        this._deletingFolder = false;
        this._apps = [];
        this._redisplay();
    }

    _createGrid() {
        return new FolderGrid();
    }

    _getItemPosition(item) {
        const appIndex = this._apps.indexOf(item.app);

        if (appIndex === -1)
            return [-1, -1];

        const {itemsPerPage} = this._grid;
        return [Math.floor(appIndex / itemsPerPage), appIndex % itemsPerPage];
    }

    _compareItems(a, b) {
        const aPosition = this._apps.indexOf(a.app);
        const bPosition = this._apps.indexOf(b.app);

        if (aPosition === -1 && bPosition === -1)
            return a.name.localeCompare(b.name);
        else if (aPosition === -1)
            return 1;
        else if (bPosition === -1)
            return -1;

        return aPosition - bPosition;
    }

    createFolderIcon(size) {
        const layout = new Clutter.GridLayout({
            row_homogeneous: true,
            column_homogeneous: true,
        });
        let icon = new St.Widget({
            layout_manager: layout,
            x_align: Clutter.ActorAlign.CENTER,
            style: `width: ${size}px; height: ${size}px;`,
        });

        let subSize = Math.floor(FOLDER_SUBICON_FRACTION * size);

        let numItems = this._orderedItems.length;
        let rtl = icon.get_text_direction() === Clutter.TextDirection.RTL;
        for (let i = 0; i < 4; i++) {
            const style = `width: ${subSize}px; height: ${subSize}px;`;
            let bin = new St.Bin({style});
            if (i < numItems)
                bin.child = this._orderedItems[i].app.create_icon_texture(subSize);
            layout.attach(bin, rtl ? (i + 1) % 2 : i % 2, Math.floor(i / 2), 1, 1);
        }

        return icon;
    }

    _loadApps() {
        this._apps = [];
        const excludedApps = this._folder.get_strv('excluded-apps');
        const appSys = Shell.AppSystem.get_default();
        const addAppId = appId => {
            if (excludedApps.includes(appId))
                return;

            if (this._appFavorites.isFavorite(appId))
                return;

            const app = appSys.lookup_app(appId);
            if (!app)
                return;

            if (!this._parentalControlsManager.shouldShowApp(app.get_app_info()))
                return;

            if (this._apps.indexOf(app) !== -1)
                return;

            this._apps.push(app);
        };

        const folderApps = this._folder.get_strv('apps');
        folderApps.forEach(addAppId);

        const folderCategories = this._folder.get_strv('categories');
        const appInfos = this._parentView.getAppInfos();
        appInfos.forEach(appInfo => {
            let appCategories = _getCategories(appInfo);
            if (!_listsIntersect(folderCategories, appCategories))
                return;

            addAppId(appInfo.get_id());
        });

        let items = [];
        this._apps.forEach(app => {
            let icon = this._items.get(app.get_id());
            if (!icon)
                icon = new AppIcon(app);

            items.push(icon);
        });

        return items;
    }

    acceptDrop(source) {
        if (!super.acceptDrop(source))
            return false;

        const folderApps = this._orderedItems.map(item => item.id);
        this._folder.set_strv('apps', folderApps);

        return true;
    }

    addApp(app) {
        let folderApps = this._folder.get_strv('apps');
        folderApps.push(app.id);

        this._folder.set_strv('apps', folderApps);

        // Also remove from 'excluded-apps' if the app id is listed
        // there. This is only possible on categories-based folders.
        let excludedApps = this._folder.get_strv('excluded-apps');
        let index = excludedApps.indexOf(app.id);
        if (index >= 0) {
            excludedApps.splice(index, 1);
            this._folder.set_strv('excluded-apps', excludedApps);
        }
    }

    removeApp(app) {
        let folderApps = this._folder.get_strv('apps');
        let index = folderApps.indexOf(app.id);
        if (index >= 0)
            folderApps.splice(index, 1);

        // Remove the folder if this is the last app icon; otherwise,
        // just remove the icon
        if (folderApps.length === 0) {
            this._deletingFolder = true;

            // Resetting all keys deletes the relocatable schema
            let keys = this._folder.settings_schema.list_keys();
            for (const key of keys)
                this._folder.reset(key);

            let settings = new Gio.Settings({schema_id: 'org.gnome.desktop.app-folders'});
            let folders = settings.get_strv('folder-children');
            folders.splice(folders.indexOf(this._id), 1);
            settings.set_strv('folder-children', folders);

            this._deletingFolder = false;
        } else {
            // If this is a categories-based folder, also add it to
            // the list of excluded apps
            const categories = this._folder.get_strv('categories');
            if (categories.length > 0) {
                const excludedApps = this._folder.get_strv('excluded-apps');
                excludedApps.push(app.id);
                this._folder.set_strv('excluded-apps', excludedApps);
            }

            this._folder.set_strv('apps', folderApps);
        }
    }

    get deletingFolder() {
        return this._deletingFolder;
    }
});

export const FolderIcon = GObject.registerClass({
    Signals: {
        'apps-changed': {},
    },
}, class FolderIcon extends AppViewItem {
    _init(id, path, parentView) {
        super._init({
            style_class: 'overview-tile app-folder',
            button_mask: St.ButtonMask.ONE,
            toggle_mode: true,
            can_focus: true,
        }, global.settings.is_writable('app-picker-layout'));
        this._id = id;
        this._name = '';
        this._parentView = parentView;

        this._folder = new Gio.Settings({
            schema_id: 'org.gnome.desktop.app-folders.folder',
            path,
        });

        this.icon = new IconGrid.BaseIcon('', {
            createIcon: this._createIcon.bind(this),
            setSizeManually: true,
        });
        this.set_child(this.icon);
        this.label_actor = this.icon.label;

        this.view = new FolderView(this._folder, id, parentView);

        this._folder.connectObject(
            'changed', this._sync.bind(this), this);
        this._sync();
    }

    _onDestroy() {
        super._onDestroy();

        if (this._dialog)
            this._dialog.destroy();
        else
            this.view.destroy();
    }

    vfunc_clicked() {
        this.open();
    }

    vfunc_unmap() {
        if (this._dialog)
            this._dialog.popdown();

        super.vfunc_unmap();
    }

    open() {
        this._ensureFolderDialog();
        this.view._scrollView.vadjustment.value = 0;
        this._dialog.popup();
    }

    getAppIds() {
        return this.view.getAllItems().map(item => item.id);
    }

    _setHoveringByDnd(hovering) {
        if (this._otherIconIsHovering === hovering)
            return;

        super._setHoveringByDnd(hovering);

        if (hovering)
            this.add_style_pseudo_class('drop');
        else
            this.remove_style_pseudo_class('drop');
    }

    _onDragMotion(dragEvent) {
        if (!this._canAccept(dragEvent.source))
            this._setHoveringByDnd(false);

        return super._onDragMotion(dragEvent);
    }

    getDragActor() {
        const iconParams = {
            createIcon: this._createIcon.bind(this),
            showLabel: this.icon.label !== null,
            setSizeManually: false,
        };

        const icon = new IconGrid.BaseIcon(this.name, iconParams);
        icon.style_class = this.style_class;

        return icon;
    }

    getDragActorSource() {
        return this;
    }

    _canAccept(source) {
        if (!(source instanceof AppIcon))
            return false;

        let view = _getViewFromIcon(source);
        if (!view || !(view instanceof AppDisplay))
            return false;

        if (this._folder.get_strv('apps').includes(source.id))
            return false;

        return true;
    }

    acceptDrop(source) {
        const accepted = super.acceptDrop(source);

        if (!accepted)
            return false;

        this.view.addApp(source.app);

        return true;
    }

    _updateName() {
        let name = _getFolderName(this._folder);
        if (this.name === name)
            return;

        this._name = name;
        this.icon.label.text = this.name;
    }

    _sync() {
        if (this.view.deletingFolder)
            return;

        this.emit('apps-changed');
        this._updateName();
        this.visible = this.view.getAllItems().length > 0;
        this.icon.update();
    }

    _createIcon(iconSize) {
        return this.view.createFolderIcon(iconSize, this);
    }

    _ensureFolderDialog() {
        if (this._dialog)
            return;
        if (!this._dialog) {
            this._dialog = new AppFolderDialog(this, this._folder,
                this._parentView);
            this._parentView.addFolderDialog(this._dialog);
            this._dialog.connect('open-state-changed', (popup, isOpen) => {
                const duration = FOLDER_DIALOG_ANIMATION_TIME / 2;
                const mode = isOpen
                    ? Clutter.AnimationMode.EASE_OUT_QUAD
                    : Clutter.AnimationMode.EASE_IN_QUAD;

                this.ease({
                    opacity: isOpen ? 0 : 255,
                    duration,
                    mode,
                    delay: isOpen ? 0 : FOLDER_DIALOG_ANIMATION_TIME - duration,
                });

                if (!isOpen)
                    this.checked = false;
            });
        }
    }
});

export const AppFolderDialog = GObject.registerClass({
    Signals: {
        'open-state-changed': {param_types: [GObject.TYPE_BOOLEAN]},
    },
}, class AppFolderDialog extends St.Bin {
    _init(source, folder, appDisplay) {
        super._init({
            visible: false,
            x_expand: true,
            y_expand: true,
            reactive: true,
        });

        this.add_constraint(new Layout.MonitorConstraint({primary: true}));

        const clickAction = new Clutter.ClickAction();
        clickAction.connect('clicked', () => {
            const [x, y] = clickAction.get_coords();
            const actor =
                global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);

            if (actor === this)
                this.popdown();
        });
        this.add_action(clickAction);

        this._source = source;
        this._folder = folder;
        this._view = source.view;
        this._appDisplay = appDisplay;
        this._delegate = this;

        this._isOpen = false;

        this._viewBox = new St.BoxLayout({
            style_class: 'app-folder-dialog',
            x_expand: true,
            y_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
            vertical: true,
        });

        this.child = new St.Bin({
            style_class: 'app-folder-dialog-container',
            child: this._viewBox,
            x_align: Clutter.ActorAlign.FILL,
            y_align: Clutter.ActorAlign.FILL,
        });

        this._addFolderNameEntry();
        this._viewBox.add_child(this._view);

        global.focus_manager.add_group(this);

        this._grabHelper = new GrabHelper.GrabHelper(this, {
            actionMode: Shell.ActionMode.POPUP,
        });
        this.connect('destroy', this._onDestroy.bind(this));

        this._dragMonitor = null;
        this._sourceMappedId = 0;
        this._popdownTimeoutId = 0;
        this._needsZoomAndFade = false;

        this._popdownCallbacks = [];
    }

    _addFolderNameEntry() {
        this._entryBox = new St.BoxLayout({
            style_class: 'folder-name-container',
        });
        this._viewBox.add_child(this._entryBox);

        // Empty actor to center the title
        let ghostButton = new Clutter.Actor();
        this._entryBox.add_child(ghostButton);

        let stack = new Shell.Stack({
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
        });
        this._entryBox.add_child(stack);

        // Folder name label
        this._folderNameLabel = new St.Label({
            style_class: 'folder-name-label',
            x_expand: true,
            y_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });

        stack.add_child(this._folderNameLabel);

        // Folder name entry
        this._entry = new St.Entry({
            style_class: 'folder-name-entry',
            opacity: 0,
            reactive: false,
        });
        this._entry.clutter_text.set({
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
        });

        this._entry.clutter_text.connect('activate', () => {
            this._showFolderLabel();
        });

        stack.add_child(this._entry);

        // Edit button
        this._editButton = new St.Button({
            style_class: 'icon-button',
            button_mask: St.ButtonMask.ONE,
            toggle_mode: true,
            reactive: true,
            can_focus: true,
            x_align: Clutter.ActorAlign.END,
            y_align: Clutter.ActorAlign.CENTER,
            icon_name: 'document-edit-symbolic',
        });

        this._editButton.connect('notify::checked', () => {
            if (this._editButton.checked)
                this._showFolderEntry();
            else
                this._showFolderLabel();
        });

        this._entryBox.add_child(this._editButton);

        ghostButton.add_constraint(new Clutter.BindConstraint({
            source: this._editButton,
            coordinate: Clutter.BindCoordinate.SIZE,
        }));

        this._folder.connect('changed::name', () => this._syncFolderName());
        this._syncFolderName();
    }

    _syncFolderName() {
        let newName = _getFolderName(this._folder);

        this._folderNameLabel.text = newName;
        this._entry.text = newName;
    }

    _switchActor(from, to) {
        to.reactive = true;
        to.ease({
            opacity: 255,
            duration: 300,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        from.ease({
            opacity: 0,
            duration: 300,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                from.reactive = false;
            },
        });
    }

    _showFolderLabel() {
        if (this._editButton.checked)
            this._editButton.checked = false;

        this._maybeUpdateFolderName();
        this._switchActor(this._entry, this._folderNameLabel);
    }

    _showFolderEntry() {
        this._switchActor(this._folderNameLabel, this._entry);

        this._entry.clutter_text.set_selection(0, -1);
        this._entry.clutter_text.grab_key_focus();
    }

    _maybeUpdateFolderName() {
        let folderName = _getFolderName(this._folder);
        let newFolderName = this._entry.text.trim();

        if (newFolderName.length === 0 || newFolderName === folderName)
            return;

        this._folder.set_string('name', newFolderName);
        this._folder.set_boolean('translate', false);
    }

    _zoomAndFadeIn() {
        let [sourceX, sourceY] =
            this._source.get_transformed_position();
        let [dialogX, dialogY] =
            this.child.get_transformed_position();

        this.child.set({
            translation_x: sourceX - dialogX,
            translation_y: sourceY - dialogY,
            scale_x: this._source.width / this.child.width,
            scale_y: this._source.height / this.child.height,
            opacity: 0,
        });

        this.ease({
            background_color: DIALOG_SHADE_NORMAL,
            duration: FOLDER_DIALOG_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
        this.child.ease({
            translation_x: 0,
            translation_y: 0,
            scale_x: 1,
            scale_y: 1,
            opacity: 255,
            duration: FOLDER_DIALOG_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        this._needsZoomAndFade = false;

        if (this._sourceMappedId === 0) {
            this._sourceMappedId = this._source.connect(
                'notify::mapped', this._zoomAndFadeOut.bind(this));
        }
    }

    _zoomAndFadeOut() {
        if (!this._isOpen)
            return;

        if (!this._source.mapped) {
            this.hide();
            return;
        }

        let [sourceX, sourceY] =
            this._source.get_transformed_position();
        let [dialogX, dialogY] =
            this.child.get_transformed_position();

        this.ease({
            background_color: Clutter.Color.from_pixel(0x00000000),
            duration: FOLDER_DIALOG_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        this.child.ease({
            translation_x: sourceX - dialogX,
            translation_y: sourceY - dialogY,
            scale_x: this._source.width / this.child.width,
            scale_y: this._source.height / this.child.height,
            opacity: 0,
            duration: FOLDER_DIALOG_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this.child.set({
                    translation_x: 0,
                    translation_y: 0,
                    scale_x: 1,
                    scale_y: 1,
                    opacity: 255,
                });
                this.hide();

                this._popdownCallbacks.forEach(func => func());
                this._popdownCallbacks = [];
            },
        });

        this._needsZoomAndFade = false;
    }

    _removeDragMonitor() {
        if (!this._dragMonitor)
            return;

        DND.removeDragMonitor(this._dragMonitor);
        this._dragMonitor = null;
    }

    _removePopdownTimeout() {
        if (this._popdownTimeoutId === 0)
            return;

        GLib.source_remove(this._popdownTimeoutId);
        this._popdownTimeoutId = 0;
    }

    _onDestroy() {
        if (this._isOpen) {
            this._isOpen = false;
            this._grabHelper.ungrab({actor: this});
            this._grabHelper = null;
        }

        if (this._sourceMappedId) {
            this._source.disconnect(this._sourceMappedId);
            this._sourceMappedId = 0;
        }

        this._removePopdownTimeout();
        this._removeDragMonitor();
    }

    vfunc_allocate(box) {
        super.vfunc_allocate(box);

        // We can only start zooming after receiving an allocation
        if (this._needsZoomAndFade)
            this._zoomAndFadeIn();
    }

    vfunc_key_press_event(event) {
        if (global.stage.get_key_focus() !== this)
            return Clutter.EVENT_PROPAGATE;

        // Since we need to only grab focus on one item child when the user
        // actually press a key we don't use navigate_focus when opening
        // the popup.
        // Instead of that, grab the focus on the AppFolderPopup actor
        // and actually moves the focus to a child only when the user
        // actually press a key.
        // It should work with just grab_key_focus on the AppFolderPopup
        // actor, but since the arrow keys are not wrapping_around the focus
        // is not grabbed by a child when the widget that has the current focus
        // is the same that is requesting focus, so to make it works with arrow
        // keys we need to connect to the key-press-event and navigate_focus
        // when that happens using TAB_FORWARD or TAB_BACKWARD instead of arrow
        // keys

        // Use TAB_FORWARD for down key and right key
        // and TAB_BACKWARD for up key and left key on ltr
        // languages
        let direction;
        let isLtr = Clutter.get_default_text_direction() === Clutter.TextDirection.LTR;
        switch (event.get_key_symbol()) {
        case Clutter.KEY_Down:
            direction = St.DirectionType.TAB_FORWARD;
            break;
        case Clutter.KEY_Right:
            direction = isLtr
                ? St.DirectionType.TAB_FORWARD
                : St.DirectionType.TAB_BACKWARD;
            break;
        case Clutter.KEY_Up:
            direction = St.DirectionType.TAB_BACKWARD;
            break;
        case Clutter.KEY_Left:
            direction = isLtr
                ? St.DirectionType.TAB_BACKWARD
                : St.DirectionType.TAB_FORWARD;
            break;
        default:
            return Clutter.EVENT_PROPAGATE;
        }
        return this.navigate_focus(null, direction, false);
    }

    _setLighterBackground(lighter) {
        const backgroundColor = lighter
            ? DIALOG_SHADE_HIGHLIGHT
            : DIALOG_SHADE_NORMAL;

        this.ease({
            backgroundColor,
            duration: FOLDER_DIALOG_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    _withinDialog(x, y) {
        const childExtents = this.child.get_transformed_extents();
        return childExtents.contains_point(new Graphene.Point({x, y}));
    }

    _setupDragMonitor() {
        if (this._dragMonitor)
            return;

        this._dragMonitor = {
            dragMotion: dragEvent => {
                const withinDialog =
                    this._withinDialog(dragEvent.x, dragEvent.y);

                this._setLighterBackground(!withinDialog);

                if (withinDialog) {
                    this._removePopdownTimeout();
                    this._removeDragMonitor();
                }
                return DND.DragMotionResult.CONTINUE;
            },
        };
        DND.addDragMonitor(this._dragMonitor);
    }

    _setupPopdownTimeout() {
        if (this._popdownTimeoutId > 0)
            return;

        this._popdownTimeoutId =
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, POPDOWN_DIALOG_TIMEOUT, () => {
                this._popdownTimeoutId = 0;
                this.popdown();
                return GLib.SOURCE_REMOVE;
            });
    }

    handleDragOver(source, actor, x, y) {
        if (this._withinDialog(x, y)) {
            this._setLighterBackground(false);
            this._removePopdownTimeout();
            this._removeDragMonitor();
        } else {
            this._setupPopdownTimeout();
            this._setupDragMonitor();
        }

        return DND.DragMotionResult.MOVE_DROP;
    }

    acceptDrop(source) {
        const appId = source.id;

        this.popdown(() => {
            this._view.removeApp(source);
            this._appDisplay.selectApp(appId);
        });

        return true;
    }

    toggle() {
        if (this._isOpen)
            this.popdown();
        else
            this.popup();
    }

    popup() {
        if (this._isOpen)
            return;

        this._isOpen = this._grabHelper.grab({
            actor: this,
            onUngrab: () => this.popdown(),
        });

        if (!this._isOpen)
            return;

        this.get_parent().set_child_above_sibling(this, null);

        this._needsZoomAndFade = true;
        this.show();

        this.emit('open-state-changed', true);
    }

    popdown(callback) {
        // Either call the callback right away, or wait for the zoom out
        // animation to finish
        if (callback) {
            if (this.visible)
                this._popdownCallbacks.push(callback);
            else
                callback();
        }

        if (!this._isOpen)
            return;

        this._zoomAndFadeOut();
        this._showFolderLabel();

        this._isOpen = false;
        this._grabHelper.ungrab({actor: this});
        this.emit('open-state-changed', false);
    }
});

export const AppIcon = GObject.registerClass({
    Signals: {
        'menu-state-changed': {param_types: [GObject.TYPE_BOOLEAN]},
        'sync-tooltip': {},
    },
}, class AppIcon extends AppViewItem {
    _init(app, iconParams = {}) {
        // Get the isDraggable property without passing it on to the BaseIcon:
        const isDraggable = iconParams['isDraggable'] ?? true;
        delete iconParams['isDraggable'];
        const expandTitleOnHover = iconParams['expandTitleOnHover'];
        delete iconParams['expandTitleOnHover'];

        super._init({style_class: 'overview-tile'}, isDraggable, expandTitleOnHover);

        this.app = app;
        this._id = app.get_id();
        this._name = app.get_name();

        this._iconContainer = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
        });

        this.set_child(this._iconContainer);

        this._folderPreviewId = 0;

        iconParams['createIcon'] = this._createIcon.bind(this);
        iconParams['setSizeManually'] = true;
        this.icon = new IconGrid.BaseIcon(app.get_name(), iconParams);
        this._iconContainer.add_child(this.icon);

        this._dot = new St.Widget({
            style_class: 'app-grid-running-dot',
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.END,
        });
        this._dot.translationY = 8;
        this._iconContainer.add_child(this._dot);

        this.label_actor = this.icon.label;

        this.connect('popup-menu', this._onKeyboardPopupMenu.bind(this));

        this._menu = null;
        this._menuManager = new PopupMenu.PopupMenuManager(this);

        this._menuTimeoutId = 0;
        this.app.connectObject('notify::state',
            () => this._updateRunningStyle(), this);
        this._updateRunningStyle();
    }

    _onDestroy() {
        super._onDestroy();

        if (this._folderPreviewId > 0) {
            GLib.source_remove(this._folderPreviewId);
            this._folderPreviewId = 0;
        }

        this._removeMenuTimeout();
    }

    _onDragBegin() {
        if (this._menu)
            this._menu.close(true);
        this._removeMenuTimeout();
        super._onDragBegin();
    }

    _createIcon(iconSize) {
        return this.app.create_icon_texture(iconSize);
    }

    _removeMenuTimeout() {
        if (this._menuTimeoutId > 0) {
            GLib.source_remove(this._menuTimeoutId);
            this._menuTimeoutId = 0;
        }
    }

    _updateRunningStyle() {
        if (this.app.state !== Shell.AppState.STOPPED)
            this._dot.show();
        else
            this._dot.hide();
    }

    _setPopupTimeout() {
        this._removeMenuTimeout();
        this._menuTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, MENU_POPUP_TIMEOUT, () => {
            this._menuTimeoutId = 0;
            this.popupMenu();
            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(this._menuTimeoutId, '[gnome-shell] this.popupMenu');
    }

    vfunc_leave_event(event) {
        const ret = super.vfunc_leave_event(event);

        this.fake_release();
        this._removeMenuTimeout();
        return ret;
    }

    vfunc_button_press_event(event) {
        const ret = super.vfunc_button_press_event(event);
        const button = event.get_button();
        if (button === 1) {
            this._setPopupTimeout();
        } else if (button === 3) {
            this.popupMenu();
            return Clutter.EVENT_STOP;
        }
        return ret;
    }

    vfunc_touch_event(event) {
        const ret = super.vfunc_touch_event(event);
        if (event.type() === Clutter.EventType.TOUCH_BEGIN)
            this._setPopupTimeout();

        return ret;
    }

    vfunc_clicked(button) {
        this._removeMenuTimeout();
        this.activate(button);
    }

    _onKeyboardPopupMenu() {
        this.popupMenu();
        this._menu.actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
    }

    getId() {
        return this.app.get_id();
    }

    popupMenu(side = St.Side.LEFT) {
        this.setForcedHighlight(true);
        this._removeMenuTimeout();
        this.fake_release();

        if (!this._menu) {
            this._menu = new AppMenu(this, side, {
                favoritesSection: true,
                showSingleWindows: true,
            });
            this._menu.setApp(this.app);
            this._menu.connect('open-state-changed', (menu, isPoppedUp) => {
                if (!isPoppedUp)
                    this._onMenuPoppedDown();
            });
            Main.overview.connectObject('hiding',
                () => this._menu.close(), this);

            Main.uiGroup.add_child(this._menu.actor);
            this._menuManager.addMenu(this._menu);
        }

        this.emit('menu-state-changed', true);

        this._menu.open(BoxPointer.PopupAnimation.FULL);
        this._menuManager.ignoreRelease();
        this.emit('sync-tooltip');

        return false;
    }

    _onMenuPoppedDown() {
        this.setForcedHighlight(false);
        this.emit('menu-state-changed', false);
    }

    activate(button) {
        let event = Clutter.get_current_event();
        let modifiers = event ? event.get_state() : 0;
        let isMiddleButton = button && button === Clutter.BUTTON_MIDDLE;
        let isCtrlPressed = (modifiers & Clutter.ModifierType.CONTROL_MASK) !== 0;
        let openNewWindow = this.app.can_open_new_window() &&
                            this.app.state === Shell.AppState.RUNNING &&
                            (isCtrlPressed || isMiddleButton);

        if (this.app.state === Shell.AppState.STOPPED || openNewWindow)
            this.animateLaunch();

        if (openNewWindow)
            this.app.open_new_window(-1);
        else
            this.app.activate();

        Main.overview.hide();
    }

    animateLaunch() {
        this.icon.animateZoomOut();
    }

    animateLaunchAtPos(x, y) {
        this.icon.animateZoomOutAtPos(x, y);
    }

    getDragActor() {
        return this.app.create_icon_texture(Main.overview.dash.iconSize);
    }

    // Returns the original actor that should align with the actor
    // we show as the item is being dragged.
    getDragActorSource() {
        return this.icon.icon;
    }

    shouldShowTooltip() {
        return this.hover && (!this._menu || !this._menu.isOpen);
    }

    _showFolderPreview() {
        this.icon.label.opacity = 0;
        this.icon.icon.ease({
            scale_x: FOLDER_SUBICON_FRACTION,
            scale_y: FOLDER_SUBICON_FRACTION,
        });
    }

    _hideFolderPreview() {
        this.icon.label.opacity = 255;
        this.icon.icon.ease({
            scale_x: 1.0,
            scale_y: 1.0,
        });
    }

    _canAccept(source) {
        let view = _getViewFromIcon(source);

        return source !== this &&
               (source instanceof this.constructor) &&
               (view instanceof AppDisplay);
    }

    _setHoveringByDnd(hovering) {
        if (this._otherIconIsHovering === hovering)
            return;

        super._setHoveringByDnd(hovering);

        if (hovering) {
            if (this._folderPreviewId > 0)
                return;

            this._folderPreviewId =
                GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
                    this.add_style_pseudo_class('drop');
                    this._showFolderPreview();
                    this._folderPreviewId = 0;
                    return GLib.SOURCE_REMOVE;
                });
        } else {
            if (this._folderPreviewId > 0) {
                GLib.source_remove(this._folderPreviewId);
                this._folderPreviewId = 0;
            }
            this._hideFolderPreview();
            this.remove_style_pseudo_class('drop');
        }
    }

    acceptDrop(source, actor, x) {
        const accepted = super.acceptDrop(source, actor, x);
        if (!accepted)
            return false;

        let view = _getViewFromIcon(this);
        let apps = [this.id, source.id];

        return view?.createFolder(apps);
    }

    cancelActions() {
        if (this._menu)
            this._menu.close(true);
        this._removeMenuTimeout();
        super.cancelActions();
    }
});

const SystemActionIcon = GObject.registerClass(
class SystemActionIcon extends Search.GridSearchResult {
    activate() {
        SystemActions.getDefault().activateAction(this.metaInfo['id']);
        Main.overview.hide();
    }
});
(uuay)windowAttentionHandler.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GObject from 'gi://GObject';
import Shell from 'gi://Shell';

import * as Main from './main.js';
import * as MessageTray from './messageTray.js';

export class WindowAttentionHandler {
    constructor() {
        this._tracker = Shell.WindowTracker.get_default();
        global.display.connectObject(
            'window-demands-attention', this._onWindowDemandsAttention.bind(this),
            'window-marked-urgent', this._onWindowDemandsAttention.bind(this),
            this);
    }

    _getTitleAndBanner(app, window) {
        let title = app.get_name();
        let banner = _('“%s” is ready').format(window.get_title());
        return [title, banner];
    }

    _onWindowDemandsAttention(display, window) {
        // We don't want to show the notification when the window is already focused,
        // because this is rather pointless.
        // Some apps (like GIMP) do things like setting the urgency hint on the
        // toolbar windows which would result into a notification even though GIMP itself is
        // focused.
        // We are just ignoring the hint on skip_taskbar windows for now.
        // (Which is the same behaviour as with metacity + panel)

        if (!window || window.has_focus() || window.is_skip_taskbar())
            return;

        let app = this._tracker.get_window_app(window);
        let source = new WindowAttentionSource(app, window);
        Main.messageTray.add(source);

        let [title, body] = this._getTitleAndBanner(app, window);

        let notification = new MessageTray.Notification({
            source,
            title,
            body,
            forFeedback: true,
        });
        notification.connect('activated', () => {
            source.open();
        });

        source.addNotification(notification);

        window.connectObject('notify::title', () => {
            [title, body] = this._getTitleAndBanner(app, window);
            notification.set({title, body});
        }, source);
    }
}

const WindowAttentionSource = GObject.registerClass(
class WindowAttentionSource extends MessageTray.Source {
    constructor(app, window) {
        super({
            title: app.get_name(),
            icon: app.get_icon(),
            policy: MessageTray.NotificationPolicy.newForApp(app),
        });

        this._window = window;
        this._window.connectObject(
            'notify::demands-attention', this._sync.bind(this),
            'notify::urgent', this._sync.bind(this),
            'focus', () => this.destroy(),
            'unmanaged', () => this.destroy(), this);
    }

    _sync() {
        if (this._window.demands_attention || this._window.urgent)
            return;
        this.destroy();
    }

    destroy(params) {
        this._window.disconnectObject(this);

        super.destroy(params);
    }

    open() {
        Main.activateWindow(this._window);
    }
});
(uuay)pointerA11yTimeout.js�import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import St from 'gi://St';
import * as Main from './main.js';
import Cairo from 'gi://cairo';

const SUCCESS_ZOOM_OUT_DURATION = 150;

const PieTimer = GObject.registerClass({
    Properties: {
        'angle': GObject.ParamSpec.double(
            'angle', 'angle', 'angle',
            GObject.ParamFlags.READWRITE,
            0, 2 * Math.PI, 0),
    },
}, class PieTimer extends St.DrawingArea {
    _init() {
        this._angle = 0;
        super._init({
            style_class: 'pie-timer',
            opacity: 0,
            visible: false,
            can_focus: false,
            reactive: false,
        });

        this.set_pivot_point(0.5, 0.5);
    }

    get angle() {
        return this._angle;
    }

    set angle(angle) {
        if (this._angle === angle)
            return;

        this._angle = angle;
        this.notify('angle');
        this.queue_repaint();
    }

    vfunc_repaint() {
        let node = this.get_theme_node();
        let backgroundColor = node.get_color('-pie-background-color');
        let borderColor = node.get_color('-pie-border-color');
        let borderWidth = node.get_length('-pie-border-width');
        let [width, height] = this.get_surface_size();
        let radius = Math.min(width / 2, height / 2);

        let startAngle = 3 * Math.PI / 2;
        let endAngle = startAngle + this._angle;

        let cr = this.get_context();
        cr.setLineCap(Cairo.LineCap.ROUND);
        cr.setLineJoin(Cairo.LineJoin.ROUND);
        cr.translate(width / 2, height / 2);

        if (this._angle < 2 * Math.PI)
            cr.moveTo(0, 0);

        cr.arc(0, 0, radius - borderWidth, startAngle, endAngle);

        if (this._angle < 2 * Math.PI)
            cr.lineTo(0, 0);

        cr.closePath();

        cr.setLineWidth(0);
        cr.setSourceColor(backgroundColor);
        cr.fillPreserve();

        cr.setLineWidth(borderWidth);
        cr.setSourceColor(borderColor);
        cr.stroke();

        cr.$dispose();
    }

    start(x, y, duration) {
        this.x = x - this.width / 2;
        this.y = y - this.height / 2;
        this.show();

        this.ease({
            opacity: 255,
            duration: duration / 4,
            mode: Clutter.AnimationMode.EASE_IN_QUAD,
        });

        this.ease_property('angle', 2 * Math.PI, {
            duration,
            mode: Clutter.AnimationMode.LINEAR,
            onComplete: this._onTransitionComplete.bind(this),
        });
    }

    _onTransitionComplete() {
        this.ease({
            scale_x: 2,
            scale_y: 2,
            opacity: 0,
            duration: SUCCESS_ZOOM_OUT_DURATION,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onStopped: () => this.destroy(),
        });
    }
});

export class PointerA11yTimeout {
    constructor() {
        let seat = Clutter.get_default_backend().get_default_seat();

        seat.connect('ptr-a11y-timeout-started', (o, device, type, timeout) => {
            let [x, y] = global.get_pointer();

            this._pieTimer = new PieTimer();
            Main.uiGroup.add_child(this._pieTimer);
            Main.uiGroup.set_child_above_sibling(this._pieTimer, null);

            this._pieTimer.start(x, y, timeout);

            if (type === Clutter.PointerA11yTimeoutType.GESTURE)
                global.display.set_cursor(Meta.Cursor.CROSSHAIR);
        });

        seat.connect('ptr-a11y-timeout-stopped', (o, device, type, clicked) => {
            if (!clicked)
                this._pieTimer.destroy();

            if (type === Clutter.PointerA11yTimeoutType.GESTURE)
                global.display.set_cursor(Meta.Cursor.DEFAULT);
        });
    }
}
(uuay)remoteAccess.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import St from 'gi://St';

import * as Main from '../main.js';
import * as PanelMenu from '../panelMenu.js';
import {SystemIndicator} from '../quickSettings.js';

// Minimum amount of time the shared indicator is visible (in micro seconds)
const MIN_SHARED_INDICATOR_VISIBLE_TIME_US = 5 * GLib.TIME_SPAN_SECOND;

export const RemoteAccessApplet = GObject.registerClass(
class RemoteAccessApplet extends SystemIndicator {
    _init() {
        super._init();

        let controller = global.backend.get_remote_access_controller();

        if (!controller)
            return;

        this._handles = new Set();

        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'media-record-symbolic';
        this._indicator.add_style_class_name('privacy-indicator');

        controller.connect('new-handle', (o, handle) => {
            this._onNewHandle(handle);
        });
        this._sync();
    }

    _isRecording() {
        // Screenshot UI screencasts have their own panel, so don't show this
        // indicator if there's only a screenshot UI screencast.
        if (Main.screenshotUI.screencast_in_progress)
            return this._handles.size > 1;

        return this._handles.size > 0;
    }

    _sync() {
        this._indicator.visible = this._isRecording();
    }

    _onStopped(handle) {
        this._handles.delete(handle);
        this._sync();
    }

    _onNewHandle(handle) {
        if (!handle.is_recording)
            return;

        this._handles.add(handle);
        handle.connect('stopped', this._onStopped.bind(this));

        this._sync();
    }
});

export const ScreenRecordingIndicator = GObject.registerClass({
    Signals: {'menu-set': {}},
}, class ScreenRecordingIndicator extends PanelMenu.ButtonBox {
    _init() {
        super._init({
            reactive: true,
            can_focus: true,
            track_hover: true,
            accessible_name: _('Stop Screencast'),
            accessible_role: Atk.Role.PUSH_BUTTON,
        });
        this.add_style_class_name('screen-recording-indicator');

        this._box = new St.BoxLayout();
        this.add_child(this._box);

        this._label = new St.Label({
            text: '0:00',
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._box.add_child(this._label);

        this._icon = new St.Icon({icon_name: 'screencast-stop-symbolic'});
        this._box.add_child(this._icon);

        this.hide();
        Main.screenshotUI.connect(
            'notify::screencast-in-progress',
            this._onScreencastInProgressChanged.bind(this));
    }

    vfunc_event(event) {
        if (event.type() === Clutter.EventType.TOUCH_BEGIN ||
            event.type() === Clutter.EventType.BUTTON_PRESS)
            Main.screenshotUI.stopScreencast();

        return Clutter.EVENT_PROPAGATE;
    }

    _updateLabel() {
        const minutes = this._secondsPassed / 60;
        const seconds = this._secondsPassed % 60;
        this._label.text = '%d:%02d'.format(minutes, seconds);
    }

    _onScreencastInProgressChanged() {
        if (Main.screenshotUI.screencast_in_progress) {
            this.show();

            this._secondsPassed = 0;
            this._updateLabel();

            this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, () => {
                this._secondsPassed += 1;
                this._updateLabel();
                return GLib.SOURCE_CONTINUE;
            });
            GLib.Source.set_name_by_id(
                this._timeoutId, '[gnome-shell] screen recording indicator tick');
        } else {
            this.hide();

            GLib.source_remove(this._timeoutId);
            delete this._timeoutId;

            delete this._secondsPassed;
        }
    }
});

export const ScreenSharingIndicator = GObject.registerClass({
    Signals: {'menu-set': {}},
}, class ScreenSharingIndicator extends PanelMenu.ButtonBox {
    _init() {
        super._init({
            reactive: true,
            can_focus: true,
            track_hover: true,
            accessible_name: _('Stop Screen Sharing'),
            accessible_role: Atk.Role.PUSH_BUTTON,
        });
        this.add_style_class_name('screen-sharing-indicator');

        this._box = new St.BoxLayout();
        this.add_child(this._box);

        let icon = new St.Icon({icon_name: 'screen-shared-symbolic'});
        this._box.add_child(icon);

        icon = new St.Icon({icon_name: 'screencast-stop-symbolic'});
        this._box.add_child(icon);

        this._controller = global.backend.get_remote_access_controller();

        this._handles = new Set();

        this._controller?.connect('new-handle',
            (o, handle) => this._onNewHandle(handle));

        this._sync();
    }

    _onNewHandle(handle) {
        // We can't possibly know about all types of screen sharing on X11, so
        // showing these controls on X11 might give a false sense of security.
        // Thus, only enable these controls when using Wayland, where we are
        // in control of sharing.
        if (!Meta.is_wayland_compositor())
            return;

        if (handle.isRecording)
            return;

        this._handles.add(handle);
        handle.connect('stopped', () => {
            this._handles.delete(handle);
            this._sync();
        });
        this._sync();
    }

    vfunc_event(event) {
        if (event.type() === Clutter.EventType.TOUCH_BEGIN ||
            event.type() === Clutter.EventType.BUTTON_PRESS)
            this._stopSharing();

        return Clutter.EVENT_PROPAGATE;
    }

    _stopSharing() {
        for (const handle of this._handles)
            handle.stop();
    }

    _hideIndicator() {
        this.hide();
        delete this._hideIndicatorId;
        return GLib.SOURCE_REMOVE;
    }

    _sync() {
        if (this._hideIndicatorId) {
            GLib.source_remove(this._hideIndicatorId);
            delete this._hideIndicatorId;
        }

        if (this._handles.size > 0) {
            if (!this.visible)
                this._visibleTimeUs = GLib.get_monotonic_time();
            this.show();
        } else if (this.visible) {
            const currentTimeUs = GLib.get_monotonic_time();
            const timeSinceVisibleUs = currentTimeUs - this._visibleTimeUs;

            if (timeSinceVisibleUs >= MIN_SHARED_INDICATOR_VISIBLE_TIME_US) {
                this._hideIndicator();
            } else {
                const timeUntilHideUs =
                    MIN_SHARED_INDICATOR_VISIBLE_TIME_US - timeSinceVisibleUs;
                this._hideIndicatorId =
                    GLib.timeout_add(GLib.PRIORITY_DEFAULT,
                        timeUntilHideUs / GLib.TIME_SPAN_MILLISECOND,
                        () => this._hideIndicator());
            }
        }
    }
});
(uuay)iconGrid.js&�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Params from '../misc/params.js';
import * as Main from './main.js';

const ICON_SIZE = 96;

const PAGE_SWITCH_TIME = 300;

/** @enum {number} */
const IconSize = {
    LARGE: 96,
    MEDIUM: 64,
    MEDIUM_SMALL: 48,
    SMALL: 32,
    SMALLER: 24,
    TINY: 16,
};

const APPICON_ANIMATION_OUT_SCALE = 3;
const APPICON_ANIMATION_OUT_TIME = 250;

const ICON_POSITION_DELAY = 10;

const defaultGridModes = [
    {
        rows: 8,
        columns: 3,
    },
    {
        rows: 6,
        columns: 4,
    },
    {
        rows: 4,
        columns: 6,
    },
    {
        rows: 3,
        columns: 8,
    },
];

const LEFT_DIVIDER_LEEWAY = 20;
const RIGHT_DIVIDER_LEEWAY = 20;

/** @enum {number} */
export const DragLocation = {
    INVALID: 0,
    START_EDGE: 1,
    ON_ICON: 2,
    END_EDGE: 3,
    EMPTY_SPACE: 4,
};

export const BaseIcon = GObject.registerClass(
class BaseIcon extends Shell.SquareBin {
    _init(label, params) {
        params = Params.parse(params, {
            createIcon: null,
            setSizeManually: false,
            showLabel: true,
        });

        let styleClass = 'overview-icon';
        if (params.showLabel)
            styleClass += ' overview-icon-with-label';

        super._init({style_class: styleClass});

        this._box = new St.BoxLayout({
            vertical: true,
            x_expand: true,
            y_expand: true,
        });
        this.set_child(this._box);

        this.iconSize = ICON_SIZE;
        this._iconBin = new St.Bin({x_align: Clutter.ActorAlign.CENTER});

        this._box.add_child(this._iconBin);

        if (params.showLabel) {
            this.label = new St.Label({text: label});
            this.label.clutter_text.set({
                x_align: Clutter.ActorAlign.CENTER,
                y_align: Clutter.ActorAlign.CENTER,
            });
            this._box.add_child(this.label);
        } else {
            this.label = null;
        }

        if (params.createIcon)
            this.createIcon = params.createIcon;
        this._setSizeManually = params.setSizeManually;

        this.icon = null;

        let cache = St.TextureCache.get_default();
        cache.connectObject(
            'icon-theme-changed', this._onIconThemeChanged.bind(this), this);
    }

    // This can be overridden by a subclass, or by the createIcon
    // parameter to _init()
    createIcon(_size) {
        throw new GObject.NotImplementedError(`createIcon in ${this.constructor.name}`);
    }

    setIconSize(size) {
        if (!this._setSizeManually)
            throw new Error('setSizeManually has to be set to use setIconsize');

        if (size === this.iconSize)
            return;

        this._createIconTexture(size);
    }

    _createIconTexture(size) {
        if (this.icon)
            this.icon.destroy();
        this.iconSize = size;
        this.icon = this.createIcon(this.iconSize);

        this._iconBin.child = this.icon;
    }

    vfunc_style_changed() {
        super.vfunc_style_changed();
        let node = this.get_theme_node();

        let size;
        if (this._setSizeManually) {
            size = this.iconSize;
        } else {
            const {scaleFactor} =
                St.ThemeContext.get_for_stage(global.stage);

            let [found, len] = node.lookup_length('icon-size', false);
            size = found ? len / scaleFactor : ICON_SIZE;
        }

        if (this.iconSize === size && this._iconBin.child)
            return;

        this._createIconTexture(size);
    }

    _onIconThemeChanged() {
        // St.Icon updates automatically
        if (!(this.icon instanceof St.Icon))
            this._createIconTexture(this.iconSize);
    }

    animateZoomOut() {
        // Animate only the child instead of the entire actor, so the
        // styles like hover and running are not applied while
        // animating.
        zoomOutActor(this.child);
    }

    animateZoomOutAtPos(x, y) {
        zoomOutActorAtPos(this.child, x, y);
    }

    update() {
        this._createIconTexture(this.iconSize);
    }
});

/**
 * @param {Clutter.Actor} actor
 */
export function zoomOutActor(actor) {
    let [x, y] = actor.get_transformed_position();
    zoomOutActorAtPos(actor, x, y);
}

function zoomOutActorAtPos(actor, x, y) {
    const monitor = Main.layoutManager.findMonitorForActor(actor);
    if (!monitor)
        return;

    const actorClone = new Clutter.Clone({
        source: actor,
        reactive: false,
    });
    let [width, height] = actor.get_transformed_size();

    actorClone.set_size(width, height);
    actorClone.set_position(x, y);
    actorClone.opacity = 255;
    actorClone.set_pivot_point(0.5, 0.5);

    Main.uiGroup.add_child(actorClone);

    // Avoid monitor edges to not zoom outside the current monitor
    let scaledWidth = width * APPICON_ANIMATION_OUT_SCALE;
    let scaledHeight = height * APPICON_ANIMATION_OUT_SCALE;
    let scaledX = x - (scaledWidth - width) / 2;
    let scaledY = y - (scaledHeight - height) / 2;
    let containedX = Math.clamp(scaledX, monitor.x, monitor.x + monitor.width - scaledWidth);
    let containedY = Math.clamp(scaledY, monitor.y, monitor.y + monitor.height - scaledHeight);

    actorClone.ease({
        scale_x: APPICON_ANIMATION_OUT_SCALE,
        scale_y: APPICON_ANIMATION_OUT_SCALE,
        translation_x: containedX - scaledX,
        translation_y: containedY - scaledY,
        opacity: 0,
        duration: APPICON_ANIMATION_OUT_TIME,
        mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        onComplete: () => actorClone.destroy(),
    });
}

function animateIconPosition(icon, box, nChangedIcons) {
    if (!icon.has_allocation() || icon.allocation.equal(box) || icon.opacity === 0) {
        icon.allocate(box);
        return false;
    }

    icon.save_easing_state();
    icon.set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD);
    icon.set_easing_delay(nChangedIcons * ICON_POSITION_DELAY);

    icon.allocate(box);

    icon.restore_easing_state();

    return true;
}

function swap(value, length) {
    return length - value - 1;
}

export const IconGridLayout = GObject.registerClass({
    Properties: {
        'allow-incomplete-pages': GObject.ParamSpec.boolean('allow-incomplete-pages',
            'Allow incomplete pages', 'Allow incomplete pages',
            GObject.ParamFlags.READWRITE,
            true),
        'column-spacing': GObject.ParamSpec.int('column-spacing',
            'Column spacing', 'Column spacing',
            GObject.ParamFlags.READWRITE,
            0, GLib.MAXINT32, 0),
        'columns-per-page': GObject.ParamSpec.int('columns-per-page',
            'Columns per page', 'Columns per page',
            GObject.ParamFlags.READWRITE,
            1, GLib.MAXINT32, 6),
        'fixed-icon-size': GObject.ParamSpec.int('fixed-icon-size',
            'Fixed icon size', 'Fixed icon size',
            GObject.ParamFlags.READWRITE,
            -1, GLib.MAXINT32, -1),
        'icon-size': GObject.ParamSpec.int('icon-size',
            'Icon size', 'Icon size',
            GObject.ParamFlags.READABLE,
            0, GLib.MAXINT32, 0),
        'last-row-align': GObject.ParamSpec.enum('last-row-align',
            'Last row align', 'Last row align',
            GObject.ParamFlags.READWRITE,
            Clutter.ActorAlign.$gtype,
            Clutter.ActorAlign.FILL),
        'max-column-spacing': GObject.ParamSpec.int('max-column-spacing',
            'Maximum column spacing', 'Maximum column spacing',
            GObject.ParamFlags.READWRITE,
            -1, GLib.MAXINT32, -1),
        'max-row-spacing': GObject.ParamSpec.int('max-row-spacing',
            'Maximum row spacing', 'Maximum row spacing',
            GObject.ParamFlags.READWRITE,
            -1, GLib.MAXINT32, -1),
        'orientation': GObject.ParamSpec.enum('orientation',
            'Orientation', 'Orientation',
            GObject.ParamFlags.READWRITE,
            Clutter.Orientation.$gtype,
            Clutter.Orientation.VERTICAL),
        'page-halign': GObject.ParamSpec.enum('page-halign',
            'Horizontal page align',
            'Horizontal page align',
            GObject.ParamFlags.READWRITE,
            Clutter.ActorAlign.$gtype,
            Clutter.ActorAlign.FILL),
        'page-padding': GObject.ParamSpec.boxed('page-padding',
            'Page padding', 'Page padding',
            GObject.ParamFlags.READWRITE,
            Clutter.Margin.$gtype),
        'page-valign': GObject.ParamSpec.enum('page-valign',
            'Vertical page align',
            'Vertical page align',
            GObject.ParamFlags.READWRITE,
            Clutter.ActorAlign.$gtype,
            Clutter.ActorAlign.FILL),
        'row-spacing': GObject.ParamSpec.int('row-spacing',
            'Row spacing', 'Row spacing',
            GObject.ParamFlags.READWRITE,
            0, GLib.MAXINT32, 0),
        'rows-per-page': GObject.ParamSpec.int('rows-per-page',
            'Rows per page', 'Rows per page',
            GObject.ParamFlags.READWRITE,
            1, GLib.MAXINT32, 4),
    },
    Signals: {
        'pages-changed': {},
    },
}, class IconGridLayout extends Clutter.LayoutManager {
    _init(params = {}) {
        this._orientation = params.orientation ?? Clutter.Orientation.VERTICAL;

        super._init(params);

        if (!this.pagePadding)
            this.pagePadding = new Clutter.Margin();

        this._iconSize = this.fixedIconSize !== -1
            ? this.fixedIconSize
            : IconSize.LARGE;

        this._pageSizeChanged = false;
        this._pageHeight = 0;
        this._pageWidth = 0;
        this._nPages = -1;

        // [
        //     {
        //         children: [ itemData, itemData, itemData, ... ],
        //     },
        //     {
        //         children: [ itemData, itemData, itemData, ... ],
        //     },
        //     {
        //         children: [ itemData, itemData, itemData, ... ],
        //     },
        // ]
        this._pages = [];

        // {
        //     item: {
        //         actor: Clutter.Actor,
        //         pageIndex: <index>,
        //     },
        //     item: {
        //         actor: Clutter.Actor,
        //         pageIndex: <index>,
        //     },
        // }
        this._items = new Map();

        this._containerDestroyedId = 0;
        this._updateIconSizesLaterId = 0;

        this._childrenMaxSize = -1;
    }

    _findBestIconSize() {
        const nColumns = this.columnsPerPage;
        const nRows = this.rowsPerPage;
        const columnSpacingPerPage = this.columnSpacing * (nColumns - 1);
        const rowSpacingPerPage = this.rowSpacing * (nRows - 1);
        const [firstItem] = this._container;

        if (this.fixedIconSize !== -1)
            return this.fixedIconSize;

        const iconSizes = Object.values(IconSize).sort((a, b) => b - a);
        for (const size of iconSizes) {
            let usedWidth, usedHeight;

            if (firstItem) {
                firstItem.icon.setIconSize(size);
                const [firstItemWidth, firstItemHeight] =
                    firstItem.get_preferred_size();

                const itemSize = Math.max(firstItemWidth, firstItemHeight);

                usedWidth = itemSize * nColumns;
                usedHeight = itemSize * nRows;
            } else {
                usedWidth = size * nColumns;
                usedHeight = size * nRows;
            }

            const emptyHSpace =
                this._pageWidth - usedWidth - columnSpacingPerPage -
                this.pagePadding.left - this.pagePadding.right;
            const emptyVSpace =
                this._pageHeight - usedHeight -  rowSpacingPerPage -
                this.pagePadding.top - this.pagePadding.bottom;

            if (emptyHSpace >= 0 && emptyVSpace > 0)
                return size;
        }

        return IconSize.TINY;
    }

    _getChildrenMaxSize() {
        if (this._childrenMaxSize === -1) {
            let minWidth = 0;
            let minHeight = 0;

            const nPages = this._pages.length;
            for (let pageIndex = 0; pageIndex < nPages; pageIndex++) {
                const page = this._pages[pageIndex];
                const nVisibleItems = page.visibleChildren.length;
                for (let itemIndex = 0; itemIndex < nVisibleItems; itemIndex++) {
                    const item = page.visibleChildren[itemIndex];

                    const childMinHeight = item.get_preferred_height(-1)[0];
                    const childMinWidth = item.get_preferred_width(-1)[0];

                    minWidth = Math.max(minWidth, childMinWidth);
                    minHeight = Math.max(minHeight, childMinHeight);
                }
            }

            this._childrenMaxSize = Math.max(minWidth, minHeight);
        }

        return this._childrenMaxSize;
    }

    _updateVisibleChildrenForPage(pageIndex) {
        this._pages[pageIndex].visibleChildren =
            this._pages[pageIndex].children.filter(actor => actor.visible);
    }

    _updatePages() {
        for (let i = 0; i < this._pages.length; i++)
            this._relocateSurplusItems(i);
    }

    _unlinkItem(item) {
        const itemData = this._items.get(item);

        item.disconnect(itemData.destroyId);
        item.disconnect(itemData.visibleId);
        item.disconnect(itemData.queueRelayoutId);

        this._items.delete(item);
    }

    _removePage(pageIndex) {
        // Make sure to not leave any icon left here
        this._pages[pageIndex].children.forEach(item => {
            this._unlinkItem(item);
        });

        // Adjust the page indexes of items after this page
        for (const itemData of this._items.values()) {
            if (itemData.pageIndex > pageIndex)
                itemData.pageIndex--;
        }

        this._pages.splice(pageIndex, 1);
        this.emit('pages-changed');
    }

    _fillItemVacancies(pageIndex) {
        if (pageIndex >= this._pages.length - 1)
            return;

        const visiblePageItems = this._pages[pageIndex].visibleChildren;
        const itemsPerPage = this.columnsPerPage * this.rowsPerPage;

        // No reduce needed
        if (visiblePageItems.length === itemsPerPage)
            return;

        const visibleNextPageItems = this._pages[pageIndex + 1].visibleChildren;
        const nMissingItems = Math.min(itemsPerPage - visiblePageItems.length, visibleNextPageItems.length);

        // Append to the current page the first items of the next page
        for (let i = 0; i < nMissingItems; i++) {
            const reducedItem = visibleNextPageItems[i];

            this._removeItemData(reducedItem);
            this._addItemToPage(reducedItem, pageIndex, -1);
        }
    }

    _removeItemData(item) {
        const itemData = this._items.get(item);
        const pageIndex = itemData.pageIndex;
        const page = this._pages[pageIndex];
        const itemIndex = page.children.indexOf(item);

        this._unlinkItem(item);

        page.children.splice(itemIndex, 1);

        this._updateVisibleChildrenForPage(pageIndex);

        // Delete the page if this is the last icon in it
        const visibleItems = this._pages[pageIndex].visibleChildren;
        if (visibleItems.length === 0)
            this._removePage(pageIndex);

        if (!this.allowIncompletePages)
            this._fillItemVacancies(pageIndex);
    }

    _relocateSurplusItems(pageIndex) {
        const visiblePageItems = this._pages[pageIndex].visibleChildren;
        const itemsPerPage = this.columnsPerPage * this.rowsPerPage;

        // No overflow needed
        if (visiblePageItems.length <= itemsPerPage)
            return;

        const nExtraItems = visiblePageItems.length - itemsPerPage;
        for (let i = 0; i < nExtraItems; i++) {
            const overflowIndex = visiblePageItems.length - i - 1;
            const overflowItem = visiblePageItems[overflowIndex];

            this._removeItemData(overflowItem);
            this._addItemToPage(overflowItem, pageIndex + 1, 0);
        }
    }

    _appendPage() {
        this._pages.push({children: []});
        this.emit('pages-changed');
    }

    _addItemToPage(item, pageIndex, index) {
        // Ensure we have at least one page
        if (this._pages.length === 0)
            this._appendPage();

        // Append a new page if necessary
        if (pageIndex === this._pages.length)
            this._appendPage();

        if (pageIndex === -1)
            pageIndex = this._pages.length - 1;

        if (index === -1)
            index = this._pages[pageIndex].children.length;

        this._items.set(item, {
            actor: item,
            pageIndex,
            destroyId: item.connect('destroy', () => this._removeItemData(item)),
            visibleId: item.connect('notify::visible', () => {
                const itemData = this._items.get(item);

                this._updateVisibleChildrenForPage(itemData.pageIndex);

                if (item.visible)
                    this._relocateSurplusItems(itemData.pageIndex);
                else if (!this.allowIncompletePages)
                    this._fillItemVacancies(itemData.pageIndex);
            }),
            queueRelayoutId: item.connect('queue-relayout', () => {
                this._childrenMaxSize = -1;
            }),
        });

        item.icon.setIconSize(this._iconSize);

        this._pages[pageIndex].children.splice(index, 0, item);
        this._updateVisibleChildrenForPage(pageIndex);
        this._relocateSurplusItems(pageIndex);
    }

    _calculateSpacing(childSize) {
        const nColumns = this.columnsPerPage;
        const nRows = this.rowsPerPage;
        const usedWidth = childSize * nColumns;
        const usedHeight = childSize * nRows;
        const columnSpacingPerPage = this.columnSpacing * (nColumns - 1);
        const rowSpacingPerPage = this.rowSpacing * (nRows - 1);

        const emptyHSpace =
            this._pageWidth - usedWidth - columnSpacingPerPage -
            this.pagePadding.left - this.pagePadding.right;
        const emptyVSpace =
            this._pageHeight - usedHeight -  rowSpacingPerPage -
            this.pagePadding.top - this.pagePadding.bottom;
        let leftEmptySpace = this.pagePadding.left;
        let topEmptySpace = this.pagePadding.top;
        let hSpacing;
        let vSpacing;

        switch (this.pageHalign) {
        case Clutter.ActorAlign.START:
            hSpacing = this.columnSpacing;
            break;
        case Clutter.ActorAlign.CENTER:
            leftEmptySpace += Math.floor(emptyHSpace / 2);
            hSpacing = this.columnSpacing;
            break;
        case Clutter.ActorAlign.END:
            leftEmptySpace += emptyHSpace;
            hSpacing = this.columnSpacing;
            break;
        case Clutter.ActorAlign.FILL:
            hSpacing = this.columnSpacing + emptyHSpace / (nColumns - 1);

            // Maybe constraint horizontal spacing
            if (this.maxColumnSpacing !== -1 && hSpacing > this.maxColumnSpacing) {
                const extraHSpacing =
                    (this.maxColumnSpacing - this.columnSpacing) * (nColumns - 1);

                hSpacing = this.maxColumnSpacing;
                leftEmptySpace +=
                    Math.max((emptyHSpace - extraHSpacing) / 2, 0);
            }
            break;
        }

        switch (this.pageValign) {
        case Clutter.ActorAlign.START:
            vSpacing = this.rowSpacing;
            break;
        case Clutter.ActorAlign.CENTER:
            topEmptySpace += Math.floor(emptyVSpace / 2);
            vSpacing = this.rowSpacing;
            break;
        case Clutter.ActorAlign.END:
            topEmptySpace += emptyVSpace;
            vSpacing = this.rowSpacing;
            break;
        case Clutter.ActorAlign.FILL:
            vSpacing = this.rowSpacing + emptyVSpace / (nRows - 1);

            // Maybe constraint vertical spacing
            if (this.maxRowSpacing !== -1 && vSpacing > this.maxRowSpacing) {
                const extraVSpacing =
                    (this.maxRowSpacing - this.rowSpacing) * (nRows - 1);

                vSpacing = this.maxRowSpacing;
                topEmptySpace +=
                    Math.max((emptyVSpace - extraVSpacing) / 2, 0);
            }

            break;
        }

        return [leftEmptySpace, topEmptySpace, hSpacing, vSpacing];
    }

    _getRowPadding(align, items, itemIndex, childSize, spacing) {
        if (align === Clutter.ActorAlign.START ||
            align === Clutter.ActorAlign.FILL)
            return 0;

        const nRows = Math.ceil(items.length / this.columnsPerPage);

        let rowAlign = 0;
        const row = Math.floor(itemIndex / this.columnsPerPage);

        // Only apply to the last row
        if (row < nRows - 1)
            return 0;

        const rowStart = row * this.columnsPerPage;
        const rowEnd = Math.min((row + 1) * this.columnsPerPage - 1, items.length - 1);
        const itemsInThisRow = rowEnd - rowStart + 1;
        const nEmpty = this.columnsPerPage - itemsInThisRow;
        const availableWidth = nEmpty * (spacing + childSize);

        const isRtl =
            Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;

        switch (align) {
        case Clutter.ActorAlign.CENTER:
            rowAlign = availableWidth / 2;
            break;
        case Clutter.ActorAlign.END:
            rowAlign = availableWidth;
            break;
        // START and FILL align are handled at the beginning of the function
        }

        return isRtl ? rowAlign * -1 : rowAlign;
    }

    _onDestroy() {
        if (this._updateIconSizesLaterId >= 0) {
            const laters = global.compositor.get_laters();
            laters.remove(this._updateIconSizesLaterId);
            this._updateIconSizesLaterId = 0;
        }
    }

    vfunc_set_container(container) {
        this._container?.disconnectObject(this);

        this._container = container;

        if (this._container)
            this._container.connectObject('destroy', this._onDestroy.bind(this), this);
    }

    vfunc_get_preferred_width(_container, _forHeight) {
        let minWidth = -1;
        let natWidth = -1;

        switch (this._orientation) {
        case Clutter.Orientation.VERTICAL:
            minWidth = IconSize.TINY;
            natWidth = this._pageWidth;
            break;

        case Clutter.Orientation.HORIZONTAL:
            minWidth = this._pageWidth * this._pages.length;
            natWidth = minWidth;
            break;
        }

        return [minWidth, natWidth];
    }

    vfunc_get_preferred_height(_container, _forWidth) {
        let minHeight = -1;
        let natHeight = -1;

        switch (this._orientation) {
        case Clutter.Orientation.VERTICAL:
            minHeight = this._pageHeight * this._pages.length;
            natHeight = minHeight;
            break;

        case Clutter.Orientation.HORIZONTAL:
            minHeight = IconSize.TINY;
            natHeight = this._pageHeight;
            break;
        }

        return [minHeight, natHeight];
    }

    vfunc_allocate() {
        if (this._pageWidth === 0 || this._pageHeight === 0)
            throw new Error('IconGridLayout.adaptToSize wasn\'t called before allocation');

        const isRtl =
            Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
        const childSize = this._getChildrenMaxSize();

        const [leftEmptySpace, topEmptySpace, hSpacing, vSpacing] =
            this._calculateSpacing(childSize);

        const childBox = new Clutter.ActorBox();

        let nChangedIcons = 0;
        const columnsPerPage = this.columnsPerPage;
        const orientation = this._orientation;
        const pageWidth = this._pageWidth;
        const pageHeight = this._pageHeight;
        const pageSizeChanged = this._pageSizeChanged;
        const lastRowAlign = this.lastRowAlign;
        const shouldEaseItems = this._shouldEaseItems;

        this._pages.forEach((page, pageIndex) => {
            if (isRtl && orientation === Clutter.Orientation.HORIZONTAL)
                pageIndex = swap(pageIndex, this._pages.length);

            page.visibleChildren.forEach((item, itemIndex) => {
                const row = Math.floor(itemIndex / columnsPerPage);
                let column = itemIndex % columnsPerPage;

                if (isRtl)
                    column = swap(column, columnsPerPage);

                const rowPadding = this._getRowPadding(lastRowAlign,
                    page.visibleChildren, itemIndex, childSize, hSpacing);

                // Icon position
                let x = leftEmptySpace + rowPadding + column * (childSize + hSpacing);
                let y = topEmptySpace + row * (childSize + vSpacing);

                // Page start
                switch (orientation) {
                case Clutter.Orientation.HORIZONTAL:
                    x += pageIndex * pageWidth;
                    break;
                case Clutter.Orientation.VERTICAL:
                    y += pageIndex * pageHeight;
                    break;
                }

                childBox.set_origin(Math.floor(x), Math.floor(y));

                const [,, naturalWidth, naturalHeight] = item.get_preferred_size();
                childBox.set_size(
                    Math.max(childSize, naturalWidth),
                    Math.max(childSize, naturalHeight));

                if (!shouldEaseItems || pageSizeChanged)
                    item.allocate(childBox);
                else if (animateIconPosition(item, childBox, nChangedIcons))
                    nChangedIcons++;
            });
        });

        this._pageSizeChanged = false;
        this._shouldEaseItems = false;
    }

    _findBestPageToAppend(startPage) {
        const itemsPerPage = this.columnsPerPage * this.rowsPerPage;

        for (let i = startPage; i < this._pages.length; i++) {
            const visibleItems = this._pages[i].visibleChildren;

            if (visibleItems.length < itemsPerPage)
                return i;
        }

        return this._pages.length;
    }

    /**
     * addItem:
     *
     * @param {Clutter.Actor} item item to append to the grid
     * @param {number} page page number
     * @param {number} index position in the page
     *
     * Adds `item` to the grid. `item` must not be part of the grid.
     *
     * If `index` exceeds the number of items per page, `item` will
     * be added to the next page.
     *
     * `page` must be a number between 0 and the number of pages.
     * Adding to the page after next will create a new page.
     */
    addItem(item, page = -1, index = -1) {
        if (this._items.has(item))
            throw new Error(`Item ${item} already added to IconGridLayout`);

        if (page > this._pages.length)
            throw new Error(`Cannot add ${item} to page ${page}`);

        if (!this._container)
            return;

        if (page !== -1 && index === -1)
            page = this._findBestPageToAppend(page);

        this._shouldEaseItems = true;

        this._container.add_child(item);
        this._addItemToPage(item, page, index);
    }

    /**
     * appendItem:
     *
     * @param {Clutter.Actor} item item to append to the grid
     *
     * Appends @item to the grid. @item must not be part of the grid.
     */
    appendItem(item) {
        this.addItem(item);
    }

    /**
     * moveItem:
     *
     * @param {Clutter.Actor} item item to move
     * @param {number} newPage new page of the item
     * @param {number} newPosition new page of the item
     *
     * Moves `item` to the grid. `item` must be part of the grid.
     */
    moveItem(item, newPage, newPosition) {
        if (!this._items.has(item))
            throw new Error(`Item ${item} is not part of the IconGridLayout`);

        this._shouldEaseItems = true;

        this._removeItemData(item);

        if (newPage !== -1 && newPosition === -1)
            newPage = this._findBestPageToAppend(newPage);

        this._addItemToPage(item, newPage, newPosition);
    }

    /**
     * removeItem:
     *
     * @param {Clutter.Actor} item item to remove from the grid
     *
     * Removes `item` to the grid. `item` must be part of the grid.
     */
    removeItem(item) {
        if (!this._items.has(item))
            throw new Error(`Item ${item} is not part of the IconGridLayout`);

        if (!this._container)
            return;

        this._shouldEaseItems = true;

        this._container.remove_child(item);
        this._removeItemData(item);
    }

    /**
     * getItemsAtPage:
     *
     * @param {number} pageIndex page index
     *
     * Retrieves the children at page @pageIndex. Children may be invisible.
     *
     * @returns {Array} an array of {Clutter.Actor}s
     */
    getItemsAtPage(pageIndex) {
        if (pageIndex >= this._pages.length)
            throw new Error(`IconGridLayout does not have page ${pageIndex}`);

        return [...this._pages[pageIndex].children];
    }

    /**
     * getItemPosition:
     *
     * Retrieves the position of `item` is its page, or -1 if `item` is not
     * part of the grid.
     *
     * @param {BaseIcon} item the item
     * @returns {[number, number]} the page and position of `item`
     */
    getItemPosition(item) {
        if (!this._items.has(item))
            return [-1, -1];

        const itemData = this._items.get(item);
        const visibleItems = this._pages[itemData.pageIndex].visibleChildren;

        return [itemData.pageIndex, visibleItems.indexOf(item)];
    }

    /**
     * getItemAt:
     *
     * Retrieves the item at @page and @position.
     *
     * @param {number} page the page
     * @param {number} position the position in page
     *
     * @returns {BaseItem} the item at @page and @position, or null
     */
    getItemAt(page, position) {
        if (page < 0 || page >= this._pages.length)
            return null;

        const visibleItems = this._pages[page].visibleChildren;

        if (position < 0 || position >= visibleItems.length)
            return null;

        return visibleItems[position];
    }

    /**
     * getItemPage:
     *
     * Retrieves the page `item` is in, or -1 if `item` is not part of the grid.
     *
     * @param {BaseIcon} item the item
     *
     * @returns {number} the page where `item` is in
     */
    getItemPage(item) {
        if (!this._items.has(item))
            return -1;

        const itemData = this._items.get(item);
        return itemData.pageIndex;
    }

    ensureIconSizeUpdated() {
        if (this._updateIconSizesLaterId === 0)
            return Promise.resolve();

        return new Promise(
            resolve => this._iconSizeUpdateResolveCbs.push(resolve));
    }

    adaptToSize(pageWidth, pageHeight) {
        if (this._pageWidth === pageWidth && this._pageHeight === pageHeight)
            return;

        this._pageWidth = pageWidth;
        this._pageHeight = pageHeight;
        this._pageSizeChanged = true;

        if (this._updateIconSizesLaterId === 0) {
            const laters = global.compositor.get_laters();
            this._updateIconSizesLaterId =
                laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                    const iconSize = this._findBestIconSize();

                    if (this._iconSize !== iconSize) {
                        this._iconSize = iconSize;

                        for (const child of this._container)
                            child.icon.setIconSize(iconSize);

                        this.notify('icon-size');
                    }

                    this._updateIconSizesLaterId = 0;
                    return GLib.SOURCE_REMOVE;
                });
        }
    }

    /**
     * getDropTarget:
     *
     * Retrieves the item located at (`x`, `y`), as well as the drag location.
     * Both `x` and `y` are relative to the grid.
     *
     * @param {number} x position of the horizontal axis
     * @param {number} y position of the vertical axis
     *
     * @returns {[BaseIcon | null, DragLocation]} the item and drag location
     * under (`x`, `y`)
     */
    getDropTarget(x, y) {
        const childSize = this._getChildrenMaxSize();
        const [leftEmptySpace, topEmptySpace, hSpacing, vSpacing] =
            this._calculateSpacing(childSize);

        const isRtl =
            Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;

        let page = this._orientation === Clutter.Orientation.VERTICAL
            ? Math.floor(y / this._pageHeight)
            : Math.floor(x / this._pageWidth);

        // Out of bounds
        if (page >= this._pages.length)
            return [0, 0, DragLocation.INVALID];

        if (isRtl && this._orientation === Clutter.Orientation.HORIZONTAL)
            page = swap(page, this._pages.length);

        // Get page-relative coordinates
        let adjX = x;
        let adjY = y;
        if (this._orientation === Clutter.Orientation.HORIZONTAL)
            adjX %= this._pageWidth;
        else
            adjY %= this._pageHeight;

        const gridWidth =
            childSize * this.columnsPerPage +
            hSpacing * (this.columnsPerPage - 1);
        const gridHeight =
            childSize * this.rowsPerPage +
            vSpacing * (this.rowsPerPage - 1);

        const inTopEmptySpace = adjY < topEmptySpace;
        const inLeftEmptySpace = adjX < leftEmptySpace;
        const inRightEmptySpace = adjX > leftEmptySpace + gridWidth;
        const inBottomEmptySpace = adjY > topEmptySpace + gridHeight;

        if (inTopEmptySpace || inBottomEmptySpace)
            return [0, 0, DragLocation.INVALID];

        const halfHSpacing = hSpacing / 2;
        const halfVSpacing = vSpacing / 2;
        const visibleItems = this._pages[page].visibleChildren;

        for (let i = 0; i < visibleItems.length; i++) {
            const item = visibleItems[i];
            const childBox = item.allocation;

            const firstInRow = i % this.columnsPerPage === 0;
            const lastInRow = i % this.columnsPerPage === this.columnsPerPage - 1;

            // Check icon boundaries
            if ((inLeftEmptySpace && firstInRow) ||
                (inRightEmptySpace && lastInRow)) {
                if (y < childBox.y1 - halfVSpacing ||
                    y > childBox.y2 + halfVSpacing)
                    continue;
            } else {
                // eslint-disable-next-line no-lonely-if
                if (x < childBox.x1 - halfHSpacing ||
                    x > childBox.x2 + halfHSpacing ||
                    y < childBox.y1 - halfVSpacing ||
                    y > childBox.y2 + halfVSpacing)
                    continue;
            }

            let dragLocation;

            if (x < childBox.x1 + LEFT_DIVIDER_LEEWAY)
                dragLocation = DragLocation.START_EDGE;
            else if (x > childBox.x2 - RIGHT_DIVIDER_LEEWAY)
                dragLocation = DragLocation.END_EDGE;
            else
                dragLocation = DragLocation.ON_ICON;

            if (isRtl) {
                if (dragLocation === DragLocation.START_EDGE)
                    dragLocation = DragLocation.END_EDGE;
                else if (dragLocation === DragLocation.END_EDGE)
                    dragLocation = DragLocation.START_EDGE;
            }

            return [page, i, dragLocation];
        }

        return [page, -1, DragLocation.EMPTY_SPACE];
    }

    get iconSize() {
        return this._iconSize;
    }

    get nPages() {
        return this._pages.length;
    }

    get orientation() {
        return this._orientation;
    }

    set orientation(v) {
        if (this._orientation === v)
            return;

        switch (v) {
        case Clutter.Orientation.VERTICAL:
            this.request_mode = Clutter.RequestMode.HEIGHT_FOR_WIDTH;
            break;
        case Clutter.Orientation.HORIZONTAL:
            this.request_mode = Clutter.RequestMode.WIDTH_FOR_HEIGHT;
            break;
        }

        this._orientation = v;
        this.notify('orientation');
    }

    get pageHeight() {
        return this._pageHeight;
    }

    get pageWidth() {
        return this._pageWidth;
    }
});

export const IconGrid = GObject.registerClass({
    Signals: {
        'pages-changed': {},
    },
}, class IconGrid extends St.Viewport {
    _init(layoutParams = {}) {
        layoutParams = Params.parse(layoutParams, {
            allow_incomplete_pages: false,
            orientation: Clutter.Orientation.HORIZONTAL,
            columns_per_page: 6,
            rows_per_page: 4,
            page_halign: Clutter.ActorAlign.FILL,
            page_padding: new Clutter.Margin(),
            page_valign: Clutter.ActorAlign.FILL,
            last_row_align: Clutter.ActorAlign.START,
            column_spacing: 0,
            row_spacing: 0,
        });
        const layoutManager = new IconGridLayout(layoutParams);
        const pagesChangedId = layoutManager.connect('pages-changed',
            () => this.emit('pages-changed'));

        super._init({
            style_class: 'icon-grid',
            layoutManager,
            x_expand: true,
            y_expand: true,
        });

        this._gridModes = defaultGridModes;
        this._currentPage = 0;
        this._currentMode = -1;

        this.connect('destroy', () => layoutManager.disconnect(pagesChangedId));
    }

    vfunc_child_added(child) {
        child._iconGridKeyFocusInId = child.connect('key-focus-in', () => {
            this._ensureItemIsVisible(child);
        });
    }

    _ensureItemIsVisible(item) {
        if (!this.contains(item))
            throw new Error(`${item} is not a child of IconGrid`);

        const itemPage = this.layout_manager.getItemPage(item);
        this.goToPage(itemPage);
    }

    _setGridMode(modeIndex) {
        if (this._currentMode === modeIndex)
            return;

        this._currentMode = modeIndex;

        if (modeIndex !== -1) {
            const newMode = this._gridModes[modeIndex];

            this.layout_manager.rows_per_page = newMode.rows;
            this.layout_manager.columns_per_page = newMode.columns;
        }
    }

    _findBestModeForSize(width, height) {
        const {pagePadding} = this.layout_manager;
        width -= pagePadding.left + pagePadding.right;
        height -= pagePadding.top + pagePadding.bottom;

        const sizeRatio = width / height;
        let closestRatio = Infinity;
        let bestMode = -1;

        for (let modeIndex in this._gridModes) {
            const mode = this._gridModes[modeIndex];
            const modeRatio = mode.columns / mode.rows;

            if (Math.abs(sizeRatio - modeRatio) < Math.abs(sizeRatio - closestRatio)) {
                closestRatio = modeRatio;
                bestMode = modeIndex;
            }
        }

        this._setGridMode(bestMode);
    }

    vfunc_child_removed(child) {
        child.disconnect(child._iconGridKeyFocusInId);
        delete child._iconGridKeyFocusInId;
    }

    vfunc_allocate(box) {
        const [width, height] = box.get_size();
        this._findBestModeForSize(width, height);
        this.layout_manager.adaptToSize(width, height);
        super.vfunc_allocate(box);
    }

    vfunc_style_changed() {
        super.vfunc_style_changed();

        const node = this.get_theme_node();
        this.layout_manager.column_spacing = node.get_length('column-spacing');
        this.layout_manager.row_spacing = node.get_length('row-spacing');

        let [found, value] = node.lookup_length('max-column-spacing', false);
        this.layout_manager.max_column_spacing = found ? value : -1;

        [found, value] = node.lookup_length('max-row-spacing', false);
        this.layout_manager.max_row_spacing = found ? value : -1;

        const padding = new Clutter.Margin();
        ['top', 'right', 'bottom', 'left'].forEach(side => {
            padding[side] = node.get_length(`page-padding-${side}`);
        });
        this.layout_manager.page_padding = padding;
    }

    /**
     * addItem:
     *
     * @param {BaseItem} item item to append to the grid
     * @param {number} page page number
     * @param {number} index position in the page
     *
     * Adds `item` to the grid. `item` must not be part of the grid.
     *
     * If `index` exceeds the number of items per page, `item` will
     * be added to the next page.
     *
     * `page` must be a number between 0 and the number of pages.
     * Adding to the page after next will create a new page.
     */
    addItem(item, page = -1, index = -1) {
        if (!(item.icon instanceof BaseIcon))
            throw new Error('Only items with a BaseIcon icon property can be added to IconGrid');

        this.layout_manager.addItem(item, page, index);
    }

    /**
     * appendItem:
     *
     * @param {Clutter.Actor} item item to append to the grid
     *
     * Appends `item` to the grid. `item` must not be part of the grid.
     */
    appendItem(item) {
        this.layout_manager.appendItem(item);
    }

    /**
     * moveItem:
     *
     * Moves `item` to the grid. `item` must be part of the grid.
     *
     * @param {Clutter.Actor} item item to move
     * @param {number} newPage new page of the item
     * @param {number} newPosition new page of the item
     */
    moveItem(item, newPage, newPosition) {
        this.layout_manager.moveItem(item, newPage, newPosition);
        this.queue_relayout();
    }

    /**
     * removeItem:
     *
     * Removes `item` to the grid. `item` must be part of the grid.
     *
     * @param {Clutter.Actor} item item to remove from the grid
     */
    removeItem(item) {
        if (!this.contains(item))
            throw new Error(`Item ${item} is not part of the IconGrid`);

        this.layout_manager.removeItem(item);
    }

    /**
     * goToPage:
     *
     * Moves the current page to `pageIndex`. `pageIndex` must be a valid page
     * number.
     *
     * @param {number} pageIndex page index
     * @param {boolean} animate animate the page transition
     */
    goToPage(pageIndex, animate = true) {
        if (pageIndex >= this.nPages)
            throw new Error(`IconGrid does not have page ${pageIndex}`);

        let newValue;
        let adjustment;
        switch (this.layout_manager.orientation) {
        case Clutter.Orientation.VERTICAL:
            adjustment = this.vadjustment;
            newValue = pageIndex * this.layout_manager.pageHeight;
            break;
        case Clutter.Orientation.HORIZONTAL:
            adjustment = this.hadjustment;
            newValue = pageIndex * this.layout_manager.pageWidth;
            break;
        }

        this._currentPage = pageIndex;

        if (!this.mapped)
            animate = false;

        global.compositor.get_laters().add(
            Meta.LaterType.BEFORE_REDRAW, () => {
                adjustment.ease(newValue, {
                    mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
                    duration: animate ? PAGE_SWITCH_TIME : 0,
                });
                return GLib.SOURCE_REMOVE;
            });
    }

    /**
     * getItemPage:
     *
     * Retrieves the page `item` is in, or -1 if `item` is not part of the grid.
     *
     * @param {BaseIcon} item the item
     *
     * @returns {number} the page where `item` is in
     */
    getItemPage(item) {
        return this.layout_manager.getItemPage(item);
    }

    /**
     * getItemPosition:
     *
     * Retrieves the position of `item` is its page, or -1 if `item` is not
     * part of the grid.
     *
     * @param {BaseIcon} item the item
     *
     * @returns {[number, number]} the page and position of `item`
     */
    getItemPosition(item) {
        if (!this.contains(item))
            return [-1, -1];

        const layoutManager = this.layout_manager;
        return layoutManager.getItemPosition(item);
    }

    /**
     * getItemAt:
     *
     * Retrieves the item at `page` and `position`.
     *
     * @param {number} page the page
     * @param {number} position the position in page
     *
     * @returns {BaseItem} the item at `page` and `position`, or null
     */
    getItemAt(page, position) {
        const layoutManager = this.layout_manager;
        return layoutManager.getItemAt(page, position);
    }

    /**
     * getItemsAtPage:
     *
     * Retrieves the children at page `page`, including invisible children.
     *
     * @param {number} page the page index
     *
     * @returns {Array} an array of {Clutter.Actor}s
     */
    getItemsAtPage(page) {
        if (page < 0 || page > this.nPages)
            throw new Error(`Page ${page} does not exist at IconGrid`);

        const layoutManager = this.layout_manager;
        return layoutManager.getItemsAtPage(page);
    }

    get currentPage() {
        return this._currentPage;
    }

    set currentPage(v) {
        this.goToPage(v);
    }

    get nPages() {
        return this.layout_manager.nPages;
    }

    setGridModes(modes) {
        this._gridModes = modes ? modes : defaultGridModes;
        this.queue_relayout();
    }

    getDropTarget(x, y) {
        const layoutManager = this.layout_manager;
        return layoutManager.getDropTarget(x, y, this._currentPage);
    }

    get itemsPerPage() {
        const layoutManager = this.layout_manager;
        return layoutManager.rows_per_page * layoutManager.columns_per_page;
    }
});
(uuay)calendar.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Main from './main.js';
import * as MessageList from './messageList.js';
import * as MessageTray from './messageTray.js';
import * as Mpris from './mpris.js';
import * as PopupMenu from './popupMenu.js';
import {ensureActorVisibleInScrollView} from '../misc/animationUtils.js';

import {formatDateWithCFormatString} from '../misc/dateUtils.js';
import {loadInterfaceXML} from '../misc/fileUtils.js';

const SHOW_WEEKDATE_KEY = 'show-weekdate';
const MAX_NOTIFICATION_BUTTONS = 3;

const NC_ = (context, str) => `${context}\u0004${str}`;

function sameYear(dateA, dateB) {
    return dateA.getYear() === dateB.getYear();
}

function sameMonth(dateA, dateB) {
    return sameYear(dateA, dateB) && (dateA.getMonth() === dateB.getMonth());
}

function sameDay(dateA, dateB) {
    return sameMonth(dateA, dateB) && (dateA.getDate() === dateB.getDate());
}

function _isWorkDay(date) {
    /* Translators: Enter 0-6 (Sunday-Saturday) for non-work days. Examples: "0" (Sunday) "6" (Saturday) "06" (Sunday and Saturday). */
    let days = C_('calendar-no-work', '06');
    return !days.includes(date.getDay().toString());
}

function _getBeginningOfDay(date) {
    let ret = new Date(date.getTime());
    ret.setHours(0);
    ret.setMinutes(0);
    ret.setSeconds(0);
    ret.setMilliseconds(0);
    return ret;
}

function _getEndOfDay(date) {
    const ret = _getBeginningOfDay(date);
    ret.setDate(ret.getDate() + 1);
    return ret;
}

function _getCalendarDayAbbreviation(dayNumber) {
    let abbreviations = [
        /* Translators: Calendar grid abbreviation for Sunday.
         *
         * NOTE: These grid abbreviations are always shown together
         * and in order, e.g. "S M T W T F S".
         */
        NC_('grid sunday', 'S'),
        /* Translators: Calendar grid abbreviation for Monday */
        NC_('grid monday', 'M'),
        /* Translators: Calendar grid abbreviation for Tuesday */
        NC_('grid tuesday', 'T'),
        /* Translators: Calendar grid abbreviation for Wednesday */
        NC_('grid wednesday', 'W'),
        /* Translators: Calendar grid abbreviation for Thursday */
        NC_('grid thursday', 'T'),
        /* Translators: Calendar grid abbreviation for Friday */
        NC_('grid friday', 'F'),
        /* Translators: Calendar grid abbreviation for Saturday */
        NC_('grid saturday', 'S'),
    ];
    return Shell.util_translate_time_string(abbreviations[dayNumber]);
}

// Abstraction for an appointment/event in a calendar

class CalendarEvent {
    constructor(id, date, end, summary) {
        this.id = id;
        this.date = date;
        this.end = end;
        this.summary = summary;
    }
}

// Interface for appointments/events - e.g. the contents of a calendar
//

export const EventSourceBase = GObject.registerClass({
    GTypeFlags: GObject.TypeFlags.ABSTRACT,
    Properties: {
        'has-calendars': GObject.ParamSpec.boolean(
            'has-calendars', 'has-calendars', 'has-calendars',
            GObject.ParamFlags.READABLE,
            false),
        'is-loading': GObject.ParamSpec.boolean(
            'is-loading', 'is-loading', 'is-loading',
            GObject.ParamFlags.READABLE,
            false),
    },
    Signals: {'changed': {}},
}, class EventSourceBase extends GObject.Object {
    /**
     * @returns {boolean}
     */
    get isLoading() {
        throw new GObject.NotImplementedError(`isLoading in ${this.constructor.name}`);
    }

    /**
     * @returns {boolean}
     */
    get hasCalendars() {
        throw new GObject.NotImplementedError(`hasCalendars in ${this.constructor.name}`);
    }

    destroy() {
    }

    requestRange(_begin, _end) {
        throw new GObject.NotImplementedError(`requestRange in ${this.constructor.name}`);
    }

    getEvents(_begin, _end) {
        throw new GObject.NotImplementedError(`getEvents in ${this.constructor.name}`);
    }

    /**
     * @param {Date} _day
     * @returns {boolean}
     */
    hasEvents(_day) {
        throw new GObject.NotImplementedError(`hasEvents in ${this.constructor.name}`);
    }
});

export const EmptyEventSource = GObject.registerClass(
class EmptyEventSource extends EventSourceBase {
    get isLoading() {
        return false;
    }

    get hasCalendars() {
        return false;
    }

    requestRange(_begin, _end) {
    }

    getEvents(_begin, _end) {
        let result = [];
        return result;
    }

    hasEvents(_day) {
        return false;
    }
});

const CalendarServerIface = loadInterfaceXML('org.gnome.Shell.CalendarServer');

const CalendarServerInfo  = Gio.DBusInterfaceInfo.new_for_xml(CalendarServerIface);

function CalendarServer() {
    return new Gio.DBusProxy({
        g_connection: Gio.DBus.session,
        g_interface_name: CalendarServerInfo.name,
        g_interface_info: CalendarServerInfo,
        g_name: 'org.gnome.Shell.CalendarServer',
        g_object_path: '/org/gnome/Shell/CalendarServer',
    });
}

function _datesEqual(a, b) {
    if (a < b)
        return false;
    else if (a > b)
        return false;
    return true;
}

/**
 * Checks whether an event overlaps a given interval
 *
 * @param {Date} e0 Beginning of the event
 * @param {Date} e1 End of the event
 * @param {Date} i0 Beginning of the interval
 * @param {Date} i1 End of the interval
 * @returns {boolean} Whether there was an overlap
 */
function _eventOverlapsInterval(e0, e1, i0, i1) {
    // This also ensures zero-length events are included
    if (e0 >= i0 && e1 < i1)
        return true;

    if (e1 <= i0)
        return false;
    if (i1 <= e0)
        return false;

    return true;
}

// an implementation that reads data from a session bus service
export const DBusEventSource = GObject.registerClass(
class DBusEventSource extends EventSourceBase {
    _init() {
        super._init();
        this._resetCache();
        this._isLoading = false;

        this._initialized = false;
        this._dbusProxy = new CalendarServer();
        this._initProxy();
    }

    async _initProxy() {
        let loaded = false;

        try {
            await this._dbusProxy.init_async(GLib.PRIORITY_DEFAULT, null);
            loaded = true;
        } catch (e) {
            // Ignore timeouts and install signals as normal, because with high
            // probability the service will appear later on, and we will get a
            // NameOwnerChanged which will finish loading
            //
            // (But still _initialized to false, because the proxy does not know
            // about the HasCalendars property and would cause an exception trying
            // to read it)
            if (!e.matches(Gio.DBusError, Gio.DBusError.TIMED_OUT)) {
                log(`Error loading calendars: ${e.message}`);
                return;
            }
        }

        this._dbusProxy.connectSignal('EventsAddedOrUpdated',
            this._onEventsAddedOrUpdated.bind(this));
        this._dbusProxy.connectSignal('EventsRemoved',
            this._onEventsRemoved.bind(this));
        this._dbusProxy.connectSignal('ClientDisappeared',
            this._onClientDisappeared.bind(this));

        this._dbusProxy.connect('notify::g-name-owner', () => {
            if (this._dbusProxy.g_name_owner)
                this._onNameAppeared();
            else
                this._onNameVanished();
        });

        this._dbusProxy.connect('g-properties-changed', () => {
            this.notify('has-calendars');
        });

        this._initialized = loaded;
        if (loaded) {
            this.notify('has-calendars');
            this._onNameAppeared();
        }
    }

    destroy() {
        this._dbusProxy.run_dispose();
    }

    get hasCalendars() {
        if (this._initialized)
            return this._dbusProxy.HasCalendars;
        else
            return false;
    }

    get isLoading() {
        return this._isLoading;
    }

    _resetCache() {
        this._events = new Map();
        this._lastRequestBegin = null;
        this._lastRequestEnd = null;
    }

    _removeMatching(uidPrefix) {
        let changed = false;
        for (const id of this._events.keys()) {
            if (id.startsWith(uidPrefix))
                changed = this._events.delete(id) || changed;
        }
        return changed;
    }

    _onNameAppeared() {
        this._initialized = true;
        this._resetCache();
        this._loadEvents(true);
    }

    _onNameVanished() {
        this._resetCache();
        this.emit('changed');
    }

    _onEventsAddedOrUpdated(dbusProxy, nameOwner, argArray) {
        const [appointments = []] = argArray;
        let changed = false;
        const handledRemovals = new Set();

        for (let n = 0; n < appointments.length; n++) {
            const [id, summary, startTime, endTime] = appointments[n];
            const date = new Date(startTime * 1000);
            const end = new Date(endTime * 1000);
            let event = new CalendarEvent(id, date, end, summary);
            /* It's a recurring event */
            if (!id.endsWith('\n')) {
                const parentId = id.substr(0, id.lastIndexOf('\n') + 1);
                if (!handledRemovals.has(parentId)) {
                    handledRemovals.add(parentId);
                    this._removeMatching(parentId);
                }
            }
            this._events.set(event.id, event);

            changed = true;
        }

        if (changed)
            this.emit('changed');
    }

    _onEventsRemoved(dbusProxy, nameOwner, argArray) {
        const [ids = []] = argArray;

        let changed = false;
        for (const id of ids)
            changed = this._removeMatching(id) || changed;

        if (changed)
            this.emit('changed');
    }

    _onClientDisappeared(dbusProxy, nameOwner, argArray) {
        let [sourceUid = ''] = argArray;
        sourceUid += '\n';

        if (this._removeMatching(sourceUid))
            this.emit('changed');
    }

    _loadEvents(forceReload) {
        // Ignore while loading
        if (!this._initialized)
            return;

        if (this._curRequestBegin && this._curRequestEnd) {
            if (forceReload) {
                this._events.clear();
                this.emit('changed');
            }
            this._dbusProxy.SetTimeRangeAsync(
                this._curRequestBegin.getTime() / 1000,
                this._curRequestEnd.getTime() / 1000,
                forceReload,
                Gio.DBusCallFlags.NONE).catch(logError);
        }
    }

    requestRange(begin, end) {
        if (!(_datesEqual(begin, this._lastRequestBegin) && _datesEqual(end, this._lastRequestEnd))) {
            this._lastRequestBegin = begin;
            this._lastRequestEnd = end;
            this._curRequestBegin = begin;
            this._curRequestEnd = end;
            this._loadEvents(true);
        }
    }

    *_getFilteredEvents(begin, end) {
        for (const event of this._events.values()) {
            if (_eventOverlapsInterval(event.date, event.end, begin, end))
                yield event;
        }
    }

    getEvents(begin, end) {
        let result = [...this._getFilteredEvents(begin, end)];

        result.sort((event1, event2) => {
            // sort events by end time on ending day
            let d1 = event1.date < begin && event1.end <= end ? event1.end : event1.date;
            let d2 = event2.date < begin && event2.end <= end ? event2.end : event2.date;
            return d1.getTime() - d2.getTime();
        });
        return result;
    }

    hasEvents(day) {
        let dayBegin = _getBeginningOfDay(day);
        let dayEnd = _getEndOfDay(day);

        const {done} = this._getFilteredEvents(dayBegin, dayEnd).next();
        return !done;
    }
});

export const Calendar = GObject.registerClass({
    Signals: {'selected-date-changed': {param_types: [GLib.DateTime.$gtype]}},
}, class Calendar extends St.Widget {
    _init() {
        this._weekStart = Shell.util_get_week_start();
        this._settings = new Gio.Settings({schema_id: 'org.gnome.desktop.calendar'});

        this._settings.connect(`changed::${SHOW_WEEKDATE_KEY}`, this._onSettingsChange.bind(this));
        this._useWeekdate = this._settings.get_boolean(SHOW_WEEKDATE_KEY);

        /**
         * Translators: The header displaying just the month name
         * standalone, when this is a month of the current year.
         * "%OB" is the new format specifier introduced in glibc 2.27,
         * in most cases you should not change it.
         */
        this._headerFormatWithoutYear = _('%OB');
        /**
         * Translators: The header displaying the month name and the year
         * number, when this is a month of a different year.  You can
         * reorder the format specifiers or add other modifications
         * according to the requirements of your language.
         * "%OB" is the new format specifier introduced in glibc 2.27,
         * in most cases you should not use the old "%B" here unless you
         * absolutely know what you are doing.
         */
        this._headerFormat = _('%OB %Y');

        // Start off with the current date
        this._selectedDate = new Date();

        this._shouldDateGrabFocus = false;

        super._init({
            style_class: 'calendar',
            layout_manager: new Clutter.GridLayout(),
            reactive: true,
        });

        this._buildHeader();
    }

    setEventSource(eventSource) {
        if (!(eventSource instanceof EventSourceBase))
            throw new Error('Event source is not valid type');

        this._eventSource = eventSource;
        this._eventSource.connect('changed', () => {
            this._rebuildCalendar();
            this._update();
        });
        this._rebuildCalendar();
        this._update();
    }

    // Sets the calendar to show a specific date
    setDate(date) {
        if (sameDay(date, this._selectedDate))
            return;

        this._selectedDate = date;

        let datetime = GLib.DateTime.new_from_unix_local(
            this._selectedDate.getTime() / 1000);
        this.emit('selected-date-changed', datetime);

        this._update();
    }

    updateTimeZone() {
        // The calendar need to be rebuilt after a time zone update because
        // the date might have changed.
        this._rebuildCalendar();
        this._update();
    }

    _buildHeader() {
        let layout = this.layout_manager;
        let offsetCols = this._useWeekdate ? 1 : 0;
        this.destroy_all_children();

        // Top line of the calendar '<| September 2009 |>'
        this._topBox = new St.BoxLayout({style_class: 'calendar-month-header'});
        layout.attach(this._topBox, 0, 0, offsetCols + 7, 1);

        this._backButton = new St.Button({
            style_class: 'calendar-change-month-back pager-button',
            icon_name: 'pan-start-symbolic',
            accessible_name: _('Previous month'),
            can_focus: true,
        });
        this._topBox.add_child(this._backButton);
        this._backButton.connect('clicked', this._onPrevMonthButtonClicked.bind(this));

        this._monthLabel = new St.Label({
            style_class: 'calendar-month-label',
            can_focus: true,
            x_align: Clutter.ActorAlign.CENTER,
            x_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._topBox.add_child(this._monthLabel);

        this._forwardButton = new St.Button({
            style_class: 'calendar-change-month-forward pager-button',
            icon_name: 'pan-end-symbolic',
            accessible_name: _('Next month'),
            can_focus: true,
        });
        this._topBox.add_child(this._forwardButton);
        this._forwardButton.connect('clicked', this._onNextMonthButtonClicked.bind(this));

        // Add weekday labels...
        //
        // We need to figure out the abbreviated localized names for the days of the week;
        // we do this by just getting the next 7 days starting from right now and then putting
        // them in the right cell in the table. It doesn't matter if we add them in order
        let iter = new Date(this._selectedDate);
        iter.setSeconds(0); // Leap second protection. Hah!
        iter.setHours(12);
        for (let i = 0; i < 7; i++) {
            // Could use formatDateWithCFormatString(iter, '%a') but that normally gives three characters
            // and we want, ideally, a single character for e.g. S M T W T F S
            let customDayAbbrev = _getCalendarDayAbbreviation(iter.getDay());
            let label = new St.Label({
                style_class: 'calendar-day-heading',
                text: customDayAbbrev,
                can_focus: true,
            });
            label.accessible_name = formatDateWithCFormatString(iter, '%A');
            let col;
            if (this.get_text_direction() === Clutter.TextDirection.RTL)
                col = 6 - (7 + iter.getDay() - this._weekStart) % 7;
            else
                col = offsetCols + (7 + iter.getDay() - this._weekStart) % 7;
            layout.attach(label, col, 1, 1, 1);
            iter.setDate(iter.getDate() + 1);
        }

        // All the children after this are days, and get removed when we update the calendar
        this._firstDayIndex = this.get_n_children();
    }

    vfunc_scroll_event(event) {
        switch (event.get_scroll_direction()) {
        case Clutter.ScrollDirection.UP:
        case Clutter.ScrollDirection.LEFT:
            this._onPrevMonthButtonClicked();
            break;
        case Clutter.ScrollDirection.DOWN:
        case Clutter.ScrollDirection.RIGHT:
            this._onNextMonthButtonClicked();
            break;
        }
        return Clutter.EVENT_PROPAGATE;
    }

    _onPrevMonthButtonClicked() {
        let newDate = new Date(this._selectedDate);
        let oldMonth = newDate.getMonth();
        if (oldMonth === 0) {
            newDate.setMonth(11);
            newDate.setFullYear(newDate.getFullYear() - 1);
            if (newDate.getMonth() !== 11) {
                let day = 32 - new Date(newDate.getFullYear() - 1, 11, 32).getDate();
                newDate = new Date(newDate.getFullYear() - 1, 11, day);
            }
        } else {
            newDate.setMonth(oldMonth - 1);
            if (newDate.getMonth() !== oldMonth - 1) {
                let day = 32 - new Date(newDate.getFullYear(), oldMonth - 1, 32).getDate();
                newDate = new Date(newDate.getFullYear(), oldMonth - 1, day);
            }
        }

        this._backButton.grab_key_focus();

        this.setDate(newDate);
    }

    _onNextMonthButtonClicked() {
        let newDate = new Date(this._selectedDate);
        let oldMonth = newDate.getMonth();
        if (oldMonth === 11) {
            newDate.setMonth(0);
            newDate.setFullYear(newDate.getFullYear() + 1);
            if (newDate.getMonth() !== 0) {
                let day = 32 - new Date(newDate.getFullYear() + 1, 0, 32).getDate();
                newDate = new Date(newDate.getFullYear() + 1, 0, day);
            }
        } else {
            newDate.setMonth(oldMonth + 1);
            if (newDate.getMonth() !== oldMonth + 1) {
                let day = 32 - new Date(newDate.getFullYear(), oldMonth + 1, 32).getDate();
                newDate = new Date(newDate.getFullYear(), oldMonth + 1, day);
            }
        }

        this._forwardButton.grab_key_focus();

        this.setDate(newDate);
    }

    _onSettingsChange() {
        this._useWeekdate = this._settings.get_boolean(SHOW_WEEKDATE_KEY);
        this._buildHeader();
        this._rebuildCalendar();
        this._update();
    }

    _rebuildCalendar() {
        let now = new Date();

        // Remove everything but the topBox and the weekday labels
        let children = this.get_children();
        for (let i = this._firstDayIndex; i < children.length; i++)
            children[i].destroy();

        this._buttons = [];

        // Start at the beginning of the week before the start of the month
        //
        // We want to show always 6 weeks (to keep the calendar menu at the same
        // height if there are no events), so we pad it according to the following
        // policy:
        //
        // 1 - If a month has 6 weeks, we place no padding (example: Dec 2012)
        // 2 - If a month has 5 weeks and it starts on week start, we pad one week
        //     before it (example: Apr 2012)
        // 3 - If a month has 5 weeks and it starts on any other day, we pad one week
        //     after it (example: Nov 2012)
        // 4 - If a month has 4 weeks, we pad one week before and one after it
        //     (example: Feb 2010)
        //
        // Actually computing the number of weeks is complex, but we know that the
        // problematic categories (2 and 4) always start on week start, and that
        // all months at the end have 6 weeks.
        let beginDate = new Date(
            this._selectedDate.getFullYear(), this._selectedDate.getMonth(), 1);

        this._calendarBegin = new Date(beginDate);
        this._markedAsToday = now;

        let daysToWeekStart = (7 + beginDate.getDay() - this._weekStart) % 7;
        let startsOnWeekStart = daysToWeekStart === 0;
        let weekPadding = startsOnWeekStart ? 7 : 0;

        beginDate.setDate(beginDate.getDate() - (weekPadding + daysToWeekStart));

        let layout = this.layout_manager;
        let iter = new Date(beginDate);
        let row = 2;
        // nRows here means 6 weeks + one header + one navbar
        let nRows = 8;
        while (row < nRows) {
            let button = new St.Button({
                // xgettext:no-javascript-format
                label: formatDateWithCFormatString(iter, C_('date day number format', '%d')),
                can_focus: true,
            });
            let rtl = button.get_text_direction() === Clutter.TextDirection.RTL;

            if (this._eventSource instanceof EmptyEventSource)
                button.reactive = false;

            button._date = new Date(iter);
            button.connect('clicked', () => {
                this._shouldDateGrabFocus = true;
                this.setDate(button._date);
                this._shouldDateGrabFocus = false;
            });

            let hasEvents = this._eventSource.hasEvents(iter);
            let styleClass = 'calendar-day';

            if (_isWorkDay(iter))
                styleClass += ' calendar-weekday';
            else
                styleClass += ' calendar-weekend';

            // Hack used in lieu of border-collapse - see gnome-shell.css
            if (row === 2)
                styleClass = `calendar-day-top ${styleClass}`;

            let leftMost = rtl
                ? iter.getDay() === (this._weekStart + 6) % 7
                : iter.getDay() === this._weekStart;
            if (leftMost)
                styleClass = `calendar-day-left ${styleClass}`;

            if (sameDay(now, iter))
                styleClass += ' calendar-today';
            else if (iter.getMonth() !== this._selectedDate.getMonth())
                styleClass += ' calendar-other-month';

            if (hasEvents)
                styleClass += ' calendar-day-with-events';

            button.style_class = styleClass;

            let offsetCols = this._useWeekdate ? 1 : 0;
            let col;
            if (rtl)
                col = 6 - (7 + iter.getDay() - this._weekStart) % 7;
            else
                col = offsetCols + (7 + iter.getDay() - this._weekStart) % 7;
            layout.attach(button, col, row, 1, 1);

            this._buttons.push(button);

            if (this._useWeekdate && iter.getDay() === 4) {
                const label = new St.Label({
                    text: formatDateWithCFormatString(iter, '%V'),
                    style_class: 'calendar-week-number',
                    can_focus: true,
                });
                let weekFormat = Shell.util_translate_time_string(N_('Week %V'));
                label.clutter_text.y_align = Clutter.ActorAlign.CENTER;
                label.accessible_name = formatDateWithCFormatString(iter, weekFormat);
                layout.attach(label, rtl ? 7 : 0, row, 1, 1);
            }

            iter.setDate(iter.getDate() + 1);

            if (iter.getDay() === this._weekStart)
                row++;
        }

        // Signal to the event source that we are interested in events
        // only from this date range
        this._eventSource.requestRange(beginDate, iter);
    }

    _update() {
        let now = new Date();

        if (sameYear(this._selectedDate, now))
            this._monthLabel.text = formatDateWithCFormatString(this._selectedDate, this._headerFormatWithoutYear);
        else
            this._monthLabel.text = formatDateWithCFormatString(this._selectedDate, this._headerFormat);

        if (!this._calendarBegin || !sameMonth(this._selectedDate, this._calendarBegin) || !sameDay(now, this._markedAsToday))
            this._rebuildCalendar();

        this._buttons.forEach(button => {
            if (sameDay(button._date, this._selectedDate)) {
                button.add_style_pseudo_class('selected');
                if (this._shouldDateGrabFocus)
                    button.grab_key_focus();
            } else {
                button.remove_style_pseudo_class('selected');
            }
        });
    }
});

export const NotificationMessage = GObject.registerClass(
class NotificationMessage extends MessageList.Message {
    constructor(notification) {
        super(notification.source);

        this.notification = notification;

        this.connect('close', () => {
            this._closed = true;
            if (this.notification)
                this.notification.destroy(MessageTray.NotificationDestroyedReason.DISMISSED);
        });
        notification.connectObject(
            'action-added', (_, action) => this._addAction(action),
            'action-removed', (_, action) => this._removeAction(action),
            'destroy', () => {
                this.notification = null;
                if (!this._closed)
                    this.close();
            }, this);

        notification.bind_property('title',
            this, 'title',
            GObject.BindingFlags.SYNC_CREATE);
        notification.bind_property('body',
            this, 'body',
            GObject.BindingFlags.SYNC_CREATE);
        notification.bind_property('use-body-markup',
            this, 'use-body-markup',
            GObject.BindingFlags.SYNC_CREATE);
        notification.bind_property('datetime',
            this, 'datetime',
            GObject.BindingFlags.SYNC_CREATE);
        notification.bind_property('gicon',
            this, 'icon',
            GObject.BindingFlags.SYNC_CREATE);

        this._actions = new Map();
        this.notification.actions.forEach(action => {
            this._addAction(action);
        });
    }

    vfunc_clicked() {
        this.notification.activate();
    }

    canClose() {
        return true;
    }

    _addAction(action) {
        if (!this._buttonBox) {
            this._buttonBox = new St.BoxLayout({
                x_expand: true,
                style_class: 'notification-buttons-bin',
            });
            this.setActionArea(this._buttonBox);
            global.focus_manager.add_group(this._buttonBox);
        }

        if (this._buttonBox.get_n_children() >= MAX_NOTIFICATION_BUTTONS)
            return;

        const button = new St.Button({
            style_class: 'notification-button',
            x_expand: true,
            label: action.label,
        });

        button.connect('clicked', () => action.activate());

        this._actions.set(action, button);
        this._buttonBox.add_child(button);
    }

    _removeAction(action) {
        this._actions.get(action)?.destroy();
        this._actions.delete(action);
    }
});

const NotificationSection = GObject.registerClass(
class NotificationSection extends MessageList.MessageListSection {
    _init() {
        super._init();

        this._nUrgent = 0;

        Main.messageTray.connect('source-added', this._sourceAdded.bind(this));
        Main.messageTray.getSources().forEach(source => {
            this._sourceAdded(Main.messageTray, source);
        });
    }

    get allowed() {
        return Main.sessionMode.hasNotifications &&
               !Main.sessionMode.isGreeter;
    }

    _sourceAdded(tray, source) {
        source.connectObject('notification-added',
            this._onNotificationAdded.bind(this), this);
    }

    _onNotificationAdded(source, notification) {
        let message = new NotificationMessage(notification);

        let isUrgent = notification.urgency === MessageTray.Urgency.CRITICAL;

        notification.connectObject(
            'destroy', () => {
                if (isUrgent)
                    this._nUrgent--;
            },
            'notify::datetime', () => {
                // The datetime property changes whenever the notification is updated
                this.moveMessage(message, isUrgent ? 0 : this._nUrgent, this.mapped);
            }, this);

        if (isUrgent) {
            // Keep track of urgent notifications to keep them on top
            this._nUrgent++;
        } else if (this.mapped) {
            // Only acknowledge non-urgent notifications in case it
            // has important actions that are inaccessible when not
            // shown as banner
            notification.acknowledged = true;
        }

        let index = isUrgent ? 0 : this._nUrgent;
        this.addMessageAtIndex(message, index, this.mapped);
    }

    vfunc_map() {
        this._messages.forEach(message => {
            if (message.notification.urgency !== MessageTray.Urgency.CRITICAL)
                message.notification.acknowledged = true;
        });
        super.vfunc_map();
    }
});

const Placeholder = GObject.registerClass(
class Placeholder extends St.BoxLayout {
    _init() {
        super._init({style_class: 'message-list-placeholder', vertical: true});
        this._date = new Date();

        this._icon = new St.Icon({icon_name: 'no-notifications-symbolic'});
        this.add_child(this._icon);

        this._label = new St.Label({text: _('No Notifications')});
        this.add_child(this._label);
    }
});

const DoNotDisturbSwitch = GObject.registerClass(
class DoNotDisturbSwitch extends PopupMenu.Switch {
    _init() {
        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.notifications',
        });

        super._init(this._settings.get_boolean('show-banners'));

        this._settings.bind('show-banners',
            this, 'state',
            Gio.SettingsBindFlags.INVERT_BOOLEAN);

        this.connect('destroy', () => {
            Gio.Settings.unbind(this, 'state');
            this._settings = null;
        });
    }
});

export const CalendarMessageList = GObject.registerClass(
class CalendarMessageList extends St.Widget {
    _init() {
        super._init({
            style_class: 'message-list',
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
        });

        this._placeholder = new Placeholder();
        this.add_child(this._placeholder);

        let box = new St.BoxLayout({
            vertical: true,
            x_expand: true,
            y_expand: true,
        });
        this.add_child(box);

        this._scrollView = new St.ScrollView({
            style_class: 'vfade',
            overlay_scrollbars: true,
            x_expand: true, y_expand: true,
        });
        box.add_child(this._scrollView);

        let hbox = new St.BoxLayout({style_class: 'message-list-controls'});
        box.add_child(hbox);

        const dndLabel = new St.Label({
            text: _('Do Not Disturb'),
            y_align: Clutter.ActorAlign.CENTER,
        });
        hbox.add_child(dndLabel);

        this._dndSwitch = new DoNotDisturbSwitch();
        this._dndButton = new St.Button({
            style_class: 'dnd-button',
            can_focus: true,
            toggle_mode: true,
            child: this._dndSwitch,
            label_actor: dndLabel,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._dndSwitch.bind_property('state',
            this._dndButton, 'checked',
            GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE);
        hbox.add_child(this._dndButton);

        this._clearButton = new St.Button({
            style_class: 'message-list-clear-button button',
            label: _('Clear'),
            can_focus: true,
            x_expand: true,
            x_align: Clutter.ActorAlign.END,
            accessible_name: C_('action', 'Clear all notifications'),
        });
        this._clearButton.connect('clicked', () => {
            this._sectionList.get_children().forEach(s => s.clear());
        });
        hbox.add_child(this._clearButton);

        this._placeholder.bind_property('visible',
            this._clearButton, 'visible',
            GObject.BindingFlags.INVERT_BOOLEAN);

        this._sectionList = new St.BoxLayout({
            style_class: 'message-list-sections',
            vertical: true,
            x_expand: true,
            y_expand: true,
        });
        this._sectionList.connectObject(
            'child-added', this._sync.bind(this),
            'child-removed', this._sync.bind(this),
            this);
        this._scrollView.child = this._sectionList;

        this._mediaSection = new Mpris.MediaSection();
        this._addSection(this._mediaSection);

        this._notificationSection = new NotificationSection();
        this._addSection(this._notificationSection);

        Main.sessionMode.connect('updated', this._sync.bind(this));
    }

    _addSection(section) {
        section.connectObject(
            'notify::visible', this._sync.bind(this),
            'notify::empty', this._sync.bind(this),
            'notify::can-clear', this._sync.bind(this),
            'destroy', () => this._sectionList.remove_child(section),
            'message-focused', (_s, messageActor) => {
                ensureActorVisibleInScrollView(this._scrollView, messageActor);
            }, this);
        this._sectionList.add_child(section);
    }

    _sync() {
        let sections = this._sectionList.get_children();
        let visible = sections.some(s => s.allowed);
        this.visible = visible;
        if (!visible)
            return;

        let empty = sections.every(s => s.empty || !s.visible);
        this._placeholder.visible = empty;

        let canClear = sections.some(s => s.canClear && s.visible);
        this._clearButton.reactive = canClear;
    }
});
(uuay)keyboard.js]�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import IBus from 'gi://IBus';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Gettext from 'gettext';
import * as Signals from '../../misc/signals.js';

import * as IBusManager from '../../misc/ibusManager.js';
import * as KeyboardManager from '../../misc/keyboardManager.js';
import * as Main from '../main.js';
import * as PopupMenu from '../popupMenu.js';
import * as PanelMenu from '../panelMenu.js';
import * as SwitcherPopup from '../switcherPopup.js';
import * as Util from '../../misc/util.js';

export const INPUT_SOURCE_TYPE_XKB = 'xkb';
export const INPUT_SOURCE_TYPE_IBUS = 'ibus';

export const LayoutMenuItem = GObject.registerClass(
class LayoutMenuItem extends PopupMenu.PopupBaseMenuItem {
    _init(displayName, shortName) {
        super._init();

        this.setOrnament(PopupMenu.Ornament.NO_DOT);

        this.label = new St.Label({
            text: displayName,
            x_expand: true,
        });
        this.indicator = new St.Label({text: shortName});
        this.add_child(this.label);
        this.add_child(this.indicator);
        this.label_actor = this.label;
    }
});

export class InputSource extends Signals.EventEmitter {
    constructor(type, id, displayName, shortName, index) {
        super();

        this.type = type;
        this.id = id;
        this.displayName = displayName;
        this._shortName = shortName;
        this.index = index;

        this.properties = null;

        this.xkbId = this._getXkbId();
    }

    get shortName() {
        return this._shortName;
    }

    set shortName(v) {
        this._shortName = v;
        this.emit('changed');
    }

    activate(interactive) {
        this.emit('activate', !!interactive);
    }

    _getXkbId() {
        let engineDesc = IBusManager.getIBusManager().getEngineDesc(this.id);
        if (!engineDesc)
            return this.id;

        if (engineDesc.variant && engineDesc.variant.length > 0)
            return `${engineDesc.layout}+${engineDesc.variant}`;
        else
            return engineDesc.layout;
    }
}

export const InputSourcePopup = GObject.registerClass(
class InputSourcePopup extends SwitcherPopup.SwitcherPopup {
    _init(items, action, actionBackward) {
        super._init(items);

        this._action = action;
        this._actionBackward = actionBackward;

        this._switcherList = new InputSourceSwitcher(this._items);
    }

    _keyPressHandler(keysym, action) {
        if (action === this._action)
            this._select(this._next());
        else if (action === this._actionBackward)
            this._select(this._previous());
        else if (keysym === Clutter.KEY_Left)
            this._select(this._previous());
        else if (keysym === Clutter.KEY_Right)
            this._select(this._next());
        else
            return Clutter.EVENT_PROPAGATE;

        return Clutter.EVENT_STOP;
    }

    _finish() {
        super._finish();

        this._items[this._selectedIndex].activate(true);
    }
});

const InputSourceSwitcher = GObject.registerClass(
class InputSourceSwitcher extends SwitcherPopup.SwitcherList {
    _init(items) {
        super._init(true);

        for (let i = 0; i < items.length; i++)
            this._addIcon(items[i]);
    }

    _addIcon(item) {
        let box = new St.BoxLayout({vertical: true});

        const symbol = new St.Bin({
            style_class: 'input-source-switcher-symbol',
            child: new St.Label({
                text: item.shortName,
                x_align: Clutter.ActorAlign.CENTER,
                y_align: Clutter.ActorAlign.CENTER,
            }),
        });
        box.add_child(symbol);

        let text = new St.Label({
            text: item.displayName,
            x_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(text);

        this.addItem(box, text);
    }
});

class InputSourceSettings extends Signals.EventEmitter {
    constructor() {
        super();

        if (this.constructor === InputSourceSettings)
            throw new TypeError(`Cannot instantiate abstract class ${this.constructor.name}`);
    }

    _emitInputSourcesChanged() {
        this.emit('input-sources-changed');
    }

    _emitKeyboardOptionsChanged() {
        this.emit('keyboard-options-changed');
    }

    _emitKeyboardModelChanged() {
        this.emit('keyboard-model-changed');
    }

    _emitPerWindowChanged() {
        this.emit('per-window-changed');
    }

    get inputSources() {
        return [];
    }

    get mruSources() {
        return [];
    }

    set mruSources(sourcesList) {
        // do nothing
    }

    get keyboardOptions() {
        return [];
    }

    get perWindow() {
        return false;
    }
}

class InputSourceSystemSettings extends InputSourceSettings {
    constructor() {
        super();

        this._BUS_NAME = 'org.freedesktop.locale1';
        this._BUS_PATH = '/org/freedesktop/locale1';
        this._BUS_IFACE = 'org.freedesktop.locale1';
        this._BUS_PROPS_IFACE = 'org.freedesktop.DBus.Properties';

        this._layouts = '';
        this._variants = '';
        this._options = '';
        this._model = '';

        this._reload();

        Gio.DBus.system.signal_subscribe(this._BUS_NAME,
            this._BUS_PROPS_IFACE,
            'PropertiesChanged',
            this._BUS_PATH,
            null,
            Gio.DBusSignalFlags.NONE,
            this._reload.bind(this));
    }

    async _reload() {
        let props;
        try {
            const result = await Gio.DBus.system.call(
                this._BUS_NAME,
                this._BUS_PATH,
                this._BUS_PROPS_IFACE,
                'GetAll',
                new GLib.Variant('(s)', [this._BUS_IFACE]),
                null, Gio.DBusCallFlags.NONE, -1, null);
            [props] = result.deepUnpack();
        } catch (e) {
            log(`Could not get properties from ${this._BUS_NAME}`);
            return;
        }

        const layouts = props['X11Layout'].unpack();
        const variants = props['X11Variant'].unpack();
        const options = props['X11Options'].unpack();
        const model = props['X11Model'].unpack();

        if (layouts !== this._layouts ||
            variants !== this._variants) {
            this._layouts = layouts;
            this._variants = variants;
            this._emitInputSourcesChanged();
        }
        if (options !== this._options) {
            this._options = options;
            this._emitKeyboardOptionsChanged();
        }
        if (model !== this._model) {
            this._model = model;
            this._emitKeyboardModelChanged();
        }
    }

    get inputSources() {
        let sourcesList = [];
        let layouts = this._layouts.split(',');
        let variants = this._variants.split(',');

        for (let i = 0; i < layouts.length && !!layouts[i]; i++) {
            let id = layouts[i];
            if (variants[i])
                id += `+${variants[i]}`;
            sourcesList.push({type: INPUT_SOURCE_TYPE_XKB, id});
        }
        return sourcesList;
    }

    get keyboardOptions() {
        return this._options.split(',');
    }

    get keyboardModel() {
        return this._model;
    }
}

class InputSourceSessionSettings extends InputSourceSettings {
    constructor() {
        super();

        this._DESKTOP_INPUT_SOURCES_SCHEMA = 'org.gnome.desktop.input-sources';
        this._KEY_INPUT_SOURCES = 'sources';
        this._KEY_MRU_SOURCES = 'mru-sources';
        this._KEY_KEYBOARD_OPTIONS = 'xkb-options';
        this._KEY_KEYBOARD_MODEL = 'xkb-model';
        this._KEY_PER_WINDOW = 'per-window';

        this._settings = new Gio.Settings({schema_id: this._DESKTOP_INPUT_SOURCES_SCHEMA});
        this._settings.connect(`changed::${this._KEY_INPUT_SOURCES}`, this._emitInputSourcesChanged.bind(this));
        this._settings.connect(`changed::${this._KEY_KEYBOARD_OPTIONS}`, this._emitKeyboardOptionsChanged.bind(this));
        this._settings.connect(`changed::${this._KEY_KEYBOARD_MODEL}`, this._emitKeyboardModelChanged.bind(this));
        this._settings.connect(`changed::${this._KEY_PER_WINDOW}`, this._emitPerWindowChanged.bind(this));
    }

    _getSourcesList(key) {
        let sourcesList = [];
        let sources = this._settings.get_value(key);
        let nSources = sources.n_children();

        for (let i = 0; i < nSources; i++) {
            let [type, id] = sources.get_child_value(i).deepUnpack();
            sourcesList.push({type, id});
        }
        return sourcesList;
    }

    get inputSources() {
        return this._getSourcesList(this._KEY_INPUT_SOURCES);
    }

    get mruSources() {
        return this._getSourcesList(this._KEY_MRU_SOURCES);
    }

    set mruSources(sourcesList) {
        let sources = GLib.Variant.new('a(ss)', sourcesList);
        this._settings.set_value(this._KEY_MRU_SOURCES, sources);
    }

    get keyboardOptions() {
        return this._settings.get_strv(this._KEY_KEYBOARD_OPTIONS);
    }

    get keyboardModel() {
        return this._settings.get_string(this._KEY_KEYBOARD_MODEL);
    }

    get perWindow() {
        return this._settings.get_boolean(this._KEY_PER_WINDOW);
    }
}

export class InputSourceManager extends Signals.EventEmitter {
    constructor() {
        super();

        // All valid input sources currently in the gsettings
        // KEY_INPUT_SOURCES list indexed by their index there
        this._inputSources = {};
        // All valid input sources currently in the gsettings
        // KEY_INPUT_SOURCES list of type INPUT_SOURCE_TYPE_IBUS
        // indexed by the IBus ID
        this._ibusSources = {};

        this._currentSource = null;

        // All valid input sources currently in the gsettings
        // KEY_INPUT_SOURCES list ordered by most recently used
        this._mruSources = [];
        this._mruSourcesBackup = null;
        this._keybindingAction =
            Main.wm.addKeybinding('switch-input-source',
                new Gio.Settings({schema_id: 'org.gnome.desktop.wm.keybindings'}),
                Meta.KeyBindingFlags.NONE,
                Shell.ActionMode.ALL,
                this._switchInputSource.bind(this));
        this._keybindingActionBackward =
            Main.wm.addKeybinding('switch-input-source-backward',
                new Gio.Settings({schema_id: 'org.gnome.desktop.wm.keybindings'}),
                Meta.KeyBindingFlags.IS_REVERSED,
                Shell.ActionMode.ALL,
                this._switchInputSource.bind(this));
        if (Main.sessionMode.isGreeter)
            this._settings = new InputSourceSystemSettings();
        else
            this._settings = new InputSourceSessionSettings();
        this._settings.connect('input-sources-changed', this._inputSourcesChanged.bind(this));
        this._settings.connect('keyboard-options-changed', this._keyboardOptionsChanged.bind(this));
        this._settings.connect('keyboard-model-changed', this._keyboardModelChanged.bind(this));

        this._xkbInfo = KeyboardManager.getXkbInfo();
        this._keyboardManager = KeyboardManager.getKeyboardManager();

        this._ibusReady = false;
        this._ibusManager = IBusManager.getIBusManager();
        this._ibusManager.connect('ready', this._ibusReadyCallback.bind(this));
        this._ibusManager.connect('properties-registered', this._ibusPropertiesRegistered.bind(this));
        this._ibusManager.connect('property-updated', this._ibusPropertyUpdated.bind(this));
        this._ibusManager.connect('set-content-type', this._ibusSetContentType.bind(this));

        global.display.connect('modifiers-accelerator-activated', this._modifiersSwitcher.bind(this));

        this._sourcesPerWindow = false;
        this._focusWindowNotifyId = 0;
        this._settings.connect('per-window-changed', this._sourcesPerWindowChanged.bind(this));
        this._sourcesPerWindowChanged();
        this._disableIBus = false;
        this._reloading = false;
    }

    reload() {
        this._reloading = true;
        this._keyboardManager.setKeyboardOptions(this._settings.keyboardOptions);
        this._keyboardManager.setKeyboardModel(this._settings.keyboardModel);
        this._inputSourcesChanged();
        this._reloading = false;
    }

    _ibusReadyCallback(im, ready) {
        if (this._ibusReady === ready)
            return;

        this._ibusReady = ready;
        this._mruSources = [];
        this._inputSourcesChanged();
    }

    _modifiersSwitcher() {
        let sourceIndexes = Object.keys(this._inputSources);
        if (sourceIndexes.length === 0) {
            KeyboardManager.releaseKeyboard();
            return true;
        }

        let is = this._currentSource;
        if (!is)
            is = this._inputSources[sourceIndexes[0]];

        let nextIndex = is.index + 1;
        if (nextIndex > sourceIndexes[sourceIndexes.length - 1])
            nextIndex = 0;

        while (!(is = this._inputSources[nextIndex]))
            nextIndex += 1;

        is.activate(true);
        return true;
    }

    _switchInputSource(display, window, binding) {
        if (this._mruSources.length < 2)
            return;

        // HACK: Fall back on simple input source switching since we
        // can't show a popup switcher while a GrabHelper grab is in
        // effect without considerable work to consolidate the usage
        // of pushModal/popModal and grabHelper. See
        // https://bugzilla.gnome.org/show_bug.cgi?id=695143 .
        if (Main.actionMode === Shell.ActionMode.POPUP) {
            this._modifiersSwitcher();
            return;
        }

        this._switcherPopup = new InputSourcePopup(
            this._mruSources, this._keybindingAction, this._keybindingActionBackward);
        this._switcherPopup.connect('destroy', () => {
            this._switcherPopup = null;
        });
        if (!this._switcherPopup.show(
            binding.is_reversed(), binding.get_name(), binding.get_mask()))
            this._switcherPopup.fadeAndDestroy();
    }

    _keyboardOptionsChanged() {
        this._keyboardManager.setKeyboardOptions(this._settings.keyboardOptions);
        this._keyboardManager.reapply();
    }

    _keyboardModelChanged() {
        this._keyboardManager.setKeyboardModel(this._settings.keyboardModel);
        this._keyboardManager.reapply();
    }

    _updateMruSettings() {
        // If IBus is not ready we don't have a full picture of all
        // the available sources, so don't update the setting
        if (!this._ibusReady)
            return;

        // If IBus is temporarily disabled, don't update the setting
        if (this._disableIBus)
            return;

        let sourcesList = [];
        for (let i = 0; i < this._mruSources.length; ++i) {
            let source = this._mruSources[i];
            sourcesList.push([source.type, source.id]);
        }

        this._settings.mruSources = sourcesList;
    }

    _currentInputSourceChanged(newSource) {
        let oldSource;
        [oldSource, this._currentSource] = [this._currentSource, newSource];

        this.emit('current-source-changed', oldSource);

        for (let i = 1; i < this._mruSources.length; ++i) {
            if (this._mruSources[i] === newSource) {
                let currentSource = this._mruSources.splice(i, 1);
                this._mruSources = currentSource.concat(this._mruSources);
                break;
            }
        }
        this._changePerWindowSource();
    }

    activateInputSource(is, interactive) {
        // The focus changes during holdKeyboard/releaseKeyboard may trick
        // the client into hiding UI containing the currently focused entry.
        // So holdKeyboard/releaseKeyboard are not called when
        // 'set-content-type' signal is received.
        // E.g. Focusing on a password entry in a popup in Xorg Firefox
        // will emit 'set-content-type' signal.
        // https://gitlab.gnome.org/GNOME/gnome-shell/issues/391
        if (!this._reloading)
            KeyboardManager.holdKeyboard();
        this._keyboardManager.apply(is.xkbId);

        // All the "xkb:..." IBus engines simply "echo" back symbols,
        // despite their naming implying differently, so we always set
        // one in order for XIM applications to work given that we set
        // XMODIFIERS=@im=ibus in the first place so that they can
        // work without restarting when/if the user adds an IBus input
        // source.
        let engine;
        if (is.type === INPUT_SOURCE_TYPE_IBUS)
            engine = is.id;
        else
            engine = 'xkb:us::eng';

        if (!this._reloading)
            this._ibusManager.setEngine(engine, KeyboardManager.releaseKeyboard);
        else
            this._ibusManager.setEngine(engine);
        this._currentInputSourceChanged(is);

        if (interactive)
            this._updateMruSettings();
    }

    _updateMruSources() {
        let sourcesList = [];
        for (let i of Object.keys(this._inputSources).sort((a, b) => a - b))
            sourcesList.push(this._inputSources[i]);

        this._keyboardManager.setUserLayouts(sourcesList.map(x => x.xkbId));

        if (!this._disableIBus && this._mruSourcesBackup) {
            this._mruSources = this._mruSourcesBackup;
            this._mruSourcesBackup = null;
        }

        // Initialize from settings when we have no MRU sources list
        if (this._mruSources.length === 0) {
            let mruSettings = this._settings.mruSources;
            for (let i = 0; i < mruSettings.length; i++) {
                let mruSettingSource = mruSettings[i];
                let mruSource = null;

                for (let j = 0; j < sourcesList.length; j++) {
                    let source = sourcesList[j];
                    if (source.type === mruSettingSource.type &&
                        source.id === mruSettingSource.id) {
                        mruSource = source;
                        break;
                    }
                }

                if (mruSource)
                    this._mruSources.push(mruSource);
            }
        }

        let mruSources = [];
        if (this._mruSources.length > 1) {
            for (let i = 0; i < this._mruSources.length; i++) {
                for (let j = 0; j < sourcesList.length; j++) {
                    if (this._mruSources[i].type === sourcesList[j].type &&
                        this._mruSources[i].id === sourcesList[j].id) {
                        mruSources = mruSources.concat(sourcesList.splice(j, 1));
                        break;
                    }
                }
            }
        }

        this._mruSources = mruSources.concat(sourcesList);
    }

    _inputSourcesChanged() {
        let sources = this._settings.inputSources;
        let nSources = sources.length;

        this._currentSource = null;
        this._inputSources = {};
        this._ibusSources = {};

        let infosList = [];
        for (let i = 0; i < nSources; i++) {
            let displayName;
            let shortName;
            let type = sources[i].type;
            let id = sources[i].id;
            let exists = false;

            if (type === INPUT_SOURCE_TYPE_XKB) {
                [exists, displayName, shortName] =
                    this._xkbInfo.get_layout_info(id);
            } else if (type === INPUT_SOURCE_TYPE_IBUS) {
                if (this._disableIBus)
                    continue;
                let engineDesc = this._ibusManager.getEngineDesc(id);
                if (engineDesc) {
                    let language = IBus.get_language_name(engineDesc.get_language());
                    let longName = engineDesc.get_longname();
                    let textdomain = engineDesc.get_textdomain();
                    if (textdomain !== '')
                        longName = Gettext.dgettext(textdomain, longName);
                    exists = true;
                    displayName = `${language} (${longName})`;
                    shortName = this._makeEngineShortName(engineDesc);
                }
            }

            if (exists)
                infosList.push({type, id, displayName, shortName});
        }

        if (infosList.length === 0) {
            let type = INPUT_SOURCE_TYPE_XKB;
            let id = KeyboardManager.DEFAULT_LAYOUT;
            let [, displayName, shortName] = this._xkbInfo.get_layout_info(id);
            infosList.push({type, id, displayName, shortName});
        }

        let inputSourcesByShortName = {};
        for (let i = 0; i < infosList.length; i++) {
            let is = new InputSource(infosList[i].type,
                infosList[i].id,
                infosList[i].displayName,
                infosList[i].shortName,
                i);
            is.connect('activate', this.activateInputSource.bind(this));

            if (!(is.shortName in inputSourcesByShortName))
                inputSourcesByShortName[is.shortName] = [];
            inputSourcesByShortName[is.shortName].push(is);

            this._inputSources[is.index] = is;

            if (is.type === INPUT_SOURCE_TYPE_IBUS)
                this._ibusSources[is.id] = is;
        }

        for (let i in this._inputSources) {
            let is = this._inputSources[i];
            if (inputSourcesByShortName[is.shortName].length > 1) {
                let sub = inputSourcesByShortName[is.shortName].indexOf(is) + 1;
                is.shortName += String.fromCharCode(0x2080 + sub);
            }
        }

        this.emit('sources-changed');

        this._updateMruSources();

        if (this._mruSources.length > 0)
            this._mruSources[0].activate(false);

        // All ibus engines are preloaded here to reduce the launching time
        // when users switch the input sources.
        this._ibusManager.preloadEngines(Object.keys(this._ibusSources));
    }

    _makeEngineShortName(engineDesc) {
        let symbol = engineDesc.get_symbol();
        if (symbol && symbol[0])
            return symbol;

        let langCode = engineDesc.get_language().split('_', 1)[0];
        if (langCode.length === 2 || langCode.length === 3)
            return langCode.toLowerCase();

        return String.fromCharCode(0x2328); // keyboard glyph
    }

    _ibusPropertiesRegistered(im, engineName, props) {
        let source = this._ibusSources[engineName];
        if (!source)
            return;

        source.properties = props;

        if (source === this._currentSource)
            this.emit('current-source-changed', null);
    }

    _ibusPropertyUpdated(im, engineName, prop) {
        let source = this._ibusSources[engineName];
        if (!source)
            return;

        if (this._updateSubProperty(source.properties, prop) &&
            source === this._currentSource)
            this.emit('current-source-changed', null);
    }

    _updateSubProperty(props, prop) {
        if (!props)
            return false;

        let p;
        for (let i = 0; (p = props.get(i)) != null; ++i) {
            if (p.get_key() === prop.get_key() && p.get_prop_type() === prop.get_prop_type()) {
                p.update(prop);
                return true;
            } else if (p.get_prop_type() === IBus.PropType.MENU) {
                if (this._updateSubProperty(p.get_sub_props(), prop))
                    return true;
            }
        }
        return false;
    }

    _ibusSetContentType(im, purpose, _hints) {
        // Avoid purpose changes while the switcher popup is shown, likely due to
        // the focus change caused by the switcher popup causing this purpose change.
        if (this._switcherPopup)
            return;
        if (purpose === IBus.InputPurpose.PASSWORD) {
            if (Object.keys(this._inputSources).length === Object.keys(this._ibusSources).length)
                return;

            if (this._disableIBus)
                return;
            this._disableIBus = true;
            this._mruSourcesBackup = this._mruSources.slice();
        } else {
            if (!this._disableIBus)
                return;
            this._disableIBus = false;
        }
        this.reload();
    }

    _getNewInputSource(current) {
        let sourceIndexes = Object.keys(this._inputSources);
        if (sourceIndexes.length === 0)
            return null;

        if (current) {
            for (let i in this._inputSources) {
                let is = this._inputSources[i];
                if (is.type === current.type &&
                    is.id === current.id)
                    return is;
            }
        }

        return this._inputSources[sourceIndexes[0]];
    }

    _getCurrentWindow() {
        if (Main.overview.visible)
            return Main.overview;
        else
            return global.display.focus_window;
    }

    _setPerWindowInputSource() {
        let window = this._getCurrentWindow();
        if (!window)
            return;

        if (!window._inputSources ||
            window._inputSources !== this._inputSources) {
            window._inputSources = this._inputSources;
            window._currentSource = this._getNewInputSource(window._currentSource);
        }

        if (window._currentSource)
            window._currentSource.activate(false);
    }

    _sourcesPerWindowChanged() {
        this._sourcesPerWindow = this._settings.perWindow;

        if (this._sourcesPerWindow && this._focusWindowNotifyId === 0) {
            this._focusWindowNotifyId = global.display.connect('notify::focus-window',
                this._setPerWindowInputSource.bind(this));
            Main.overview.connectObject(
                'showing', this._setPerWindowInputSource.bind(this),
                'hidden', this._setPerWindowInputSource.bind(this), this);
        } else if (!this._sourcesPerWindow && this._focusWindowNotifyId !== 0) {
            global.display.disconnect(this._focusWindowNotifyId);
            this._focusWindowNotifyId = 0;
            Main.overview.disconnectObject(this);

            let windows = global.get_window_actors().map(w => w.meta_window);
            for (let i = 0; i < windows.length; ++i) {
                delete windows[i]._inputSources;
                delete windows[i]._currentSource;
            }
            delete Main.overview._inputSources;
            delete Main.overview._currentSource;
        }
    }

    _changePerWindowSource() {
        if (!this._sourcesPerWindow)
            return;

        let window = this._getCurrentWindow();
        if (!window)
            return;

        window._inputSources = this._inputSources;
        window._currentSource = this._currentSource;
    }

    get currentSource() {
        return this._currentSource;
    }

    get inputSources() {
        return this._inputSources;
    }

    get keyboardManager() {
        return this._keyboardManager;
    }
}

let _inputSourceManager = null;

/**
 * @returns {InputSourceManager}
 */
export function getInputSourceManager() {
    if (_inputSourceManager == null)
        _inputSourceManager = new InputSourceManager();
    return _inputSourceManager;
}

const InputSourceIndicatorContainer = GObject.registerClass(
class InputSourceIndicatorContainer extends St.Widget {
    vfunc_get_preferred_width(forHeight) {
        // Here, and in vfunc_get_preferred_height, we need to query
        // for the height of all children, but we ignore the results
        // for those we don't actually display.
        return this.get_children().reduce((maxWidth, child) => {
            let width = child.get_preferred_width(forHeight);
            return [
                Math.max(maxWidth[0], width[0]),
                Math.max(maxWidth[1], width[1]),
            ];
        }, [0, 0]);
    }

    vfunc_get_preferred_height(forWidth) {
        return this.get_children().reduce((maxHeight, child) => {
            let height = child.get_preferred_height(forWidth);
            return [
                Math.max(maxHeight[0], height[0]),
                Math.max(maxHeight[1], height[1]),
            ];
        }, [0, 0]);
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        // translate box to (0, 0)
        box.x2 -= box.x1;
        box.x1 = 0;
        box.y2 -= box.y1;
        box.y1 = 0;

        this.get_children().forEach(c => {
            c.allocate_align_fill(box, 0.5, 0.5, false, false);
        });
    }
});

export const InputSourceIndicator = GObject.registerClass(
class InputSourceIndicator extends PanelMenu.Button {
    _init() {
        super._init(0.5, _('Keyboard'));

        this.connect('destroy', this._onDestroy.bind(this));

        this._menuItems = {};
        this._indicatorLabels = {};

        this._container = new InputSourceIndicatorContainer({style_class: 'system-status-icon'});
        this.add_child(this._container);

        this._propSeparator = new PopupMenu.PopupSeparatorMenuItem();
        this.menu.addMenuItem(this._propSeparator);
        this._propSection = new PopupMenu.PopupMenuSection();
        this.menu.addMenuItem(this._propSection);
        this._propSection.actor.hide();

        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this._showLayoutItem = this.menu.addAction(_('Show Keyboard Layout'), this._showLayout.bind(this));
        this.menu.addSettingsAction(_('Keyboard Settings'),
            'gnome-keyboard-panel.desktop');

        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
        this._sessionUpdated();

        this._inputSourceManager = getInputSourceManager();
        this._inputSourceManager.connectObject(
            'sources-changed', this._sourcesChanged.bind(this),
            'current-source-changed', this._currentSourceChanged.bind(this), this);
        this._inputSourceManager.reload();
    }

    _onDestroy() {
        this._inputSourceManager = null;
    }

    _sessionUpdated() {
        // re-using "allowSettings" for the keyboard layout is a bit shady,
        // but at least for now it is used as "allow popping up windows
        // from shell menus"; we can always add a separate sessionMode
        // option if need arises.
        this._showLayoutItem.visible = Main.sessionMode.allowSettings;
    }

    _sourcesChanged() {
        for (let i in this._menuItems)
            this._menuItems[i].destroy();
        for (let i in this._indicatorLabels)
            this._indicatorLabels[i].destroy();

        this._menuItems = {};
        this._indicatorLabels = {};

        let menuIndex = 0;
        for (let i in this._inputSourceManager.inputSources) {
            let is = this._inputSourceManager.inputSources[i];

            let menuItem = new LayoutMenuItem(is.displayName, is.shortName);
            menuItem.connect('activate', () => is.activate(true));

            const indicatorLabel = new St.Label({
                text: is.shortName,
                visible: false,
            });

            this._menuItems[i] = menuItem;
            this._indicatorLabels[i] = indicatorLabel;
            is.connect('changed', () => {
                menuItem.indicator.set_text(is.shortName);
                indicatorLabel.set_text(is.shortName);
            });

            this.menu.addMenuItem(menuItem, menuIndex++);
            this._container.add_child(indicatorLabel);
        }
    }

    _currentSourceChanged(manager, oldSource) {
        let nVisibleSources = Object.keys(this._inputSourceManager.inputSources).length;
        let newSource = this._inputSourceManager.currentSource;

        if (oldSource) {
            this._menuItems[oldSource.index].setOrnament(PopupMenu.Ornament.NO_DOT);
            this._indicatorLabels[oldSource.index].hide();
        }

        if (!newSource || (nVisibleSources < 2 && !newSource.properties)) {
            // This source index might be invalid if we weren't able
            // to build a menu item for it, so we hide ourselves since
            // we can't fix it here. *shrug*

            // We also hide if we have only one visible source unless
            // it's an IBus source with properties.
            this.menu.close();
            this.hide();
            return;
        }

        this.show();

        this._buildPropSection(newSource.properties);

        this._menuItems[newSource.index].setOrnament(PopupMenu.Ornament.DOT);
        this._indicatorLabels[newSource.index].show();
    }

    _buildPropSection(properties) {
        this._propSeparator.hide();
        this._propSection.actor.hide();
        this._propSection.removeAll();

        this._buildPropSubMenu(this._propSection, properties);

        if (!this._propSection.isEmpty()) {
            this._propSection.actor.show();
            this._propSeparator.show();
        }
    }

    _buildPropSubMenu(menu, props) {
        if (!props)
            return;

        let ibusManager = IBusManager.getIBusManager();
        let radioGroup = [];
        let p;
        for (let i = 0; (p = props.get(i)) != null; ++i) {
            let prop = p;

            if (!prop.get_visible())
                continue;

            if (prop.get_key() === 'InputMode') {
                let text;
                if (prop.get_symbol)
                    text = prop.get_symbol().get_text();
                else
                    text = prop.get_label().get_text();

                let currentSource = this._inputSourceManager.currentSource;
                if (currentSource) {
                    let indicatorLabel = this._indicatorLabels[currentSource.index];
                    if (text && text.length > 0 && text.length < 3)
                        indicatorLabel.set_text(text);
                }
            }

            let item;
            let type = prop.get_prop_type();
            switch (type) {
            case IBus.PropType.MENU:
                item = new PopupMenu.PopupSubMenuMenuItem(prop.get_label().get_text());
                this._buildPropSubMenu(item.menu, prop.get_sub_props());
                break;

            case IBus.PropType.RADIO:
                item = new PopupMenu.PopupMenuItem(prop.get_label().get_text());
                item.prop = prop;
                radioGroup.push(item);
                item.radioGroup = radioGroup;
                item.setOrnament(prop.get_state() === IBus.PropState.CHECKED
                    ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NO_DOT);
                item.connect('activate', () => {
                    if (item.prop.get_state() === IBus.PropState.CHECKED)
                        return;

                    let group = item.radioGroup;
                    for (let j = 0; j < group.length; ++j) {
                        if (group[j] === item) {
                            item.setOrnament(PopupMenu.Ornament.DOT);
                            item.prop.set_state(IBus.PropState.CHECKED);
                            ibusManager.activateProperty(
                                item.prop.get_key(), IBus.PropState.CHECKED);
                        } else {
                            group[j].setOrnament(PopupMenu.Ornament.NO_DOT);
                            group[j].prop.set_state(IBus.PropState.UNCHECKED);
                            ibusManager.activateProperty(
                                group[j].prop.get_key(), IBus.PropState.UNCHECKED);
                        }
                    }
                });
                break;

            case IBus.PropType.TOGGLE:
                item = new PopupMenu.PopupSwitchMenuItem(prop.get_label().get_text(), prop.get_state() === IBus.PropState.CHECKED);
                item.prop = prop;
                item.connect('toggled', () => {
                    if (item.state) {
                        item.prop.set_state(IBus.PropState.CHECKED);
                        ibusManager.activateProperty(
                            item.prop.get_key(), IBus.PropState.CHECKED);
                    } else {
                        item.prop.set_state(IBus.PropState.UNCHECKED);
                        ibusManager.activateProperty(
                            item.prop.get_key(), IBus.PropState.UNCHECKED);
                    }
                });
                break;

            case IBus.PropType.NORMAL:
                item = new PopupMenu.PopupMenuItem(prop.get_label().get_text());
                item.prop = prop;
                item.connect('activate', () => {
                    ibusManager.activateProperty(
                        item.prop.get_key(), item.prop.get_state());
                });
                break;

            case IBus.PropType.SEPARATOR:
                item = new PopupMenu.PopupSeparatorMenuItem();
                break;

            default:
                log(`IBus property ${prop.get_key()} has invalid type ${type}`);
                continue;
            }

            item.setSensitive(prop.get_sensitive());
            menu.addMenuItem(item);
        }
    }

    _showLayout() {
        Main.overview.hide();

        let source = this._inputSourceManager.currentSource;
        let xkbLayout = '';
        let xkbVariant = '';

        if (source.type === INPUT_SOURCE_TYPE_XKB) {
            [, , , xkbLayout, xkbVariant] = KeyboardManager.getXkbInfo().get_layout_info(source.id);
        } else if (source.type === INPUT_SOURCE_TYPE_IBUS) {
            let engineDesc = IBusManager.getIBusManager().getEngineDesc(source.id);
            if (engineDesc) {
                xkbLayout = engineDesc.get_layout();
                xkbVariant = engineDesc.get_layout_variant();
            }

            // The `default` layout from ibus engine means to
            // use the current keyboard layout.
            if (xkbLayout === 'default') {
                const current = this._inputSourceManager.keyboardManager.currentLayout;
                xkbLayout = current.layout;
                xkbVariant = current.variant;
            }
        }

        if (!xkbLayout || xkbLayout.length === 0)
            return;

        let description = xkbLayout;
        if (xkbVariant.length > 0)
            description = `${description}\t${xkbVariant}`;

        Util.spawn(['gkbd-keyboard-display', '-l', description]);
    }
});
(uuay)screenshot.jsS�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Cogl from 'gi://Cogl';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import GLib from 'gi://GLib';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Mtk from 'gi://Mtk';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as GrabHelper from './grabHelper.js';
import * as Layout from './layout.js';
import * as Lightbox from './lightbox.js';
import * as Main from './main.js';
import * as MessageTray from './messageTray.js';
import * as Workspace from './workspace.js';

Gio._promisify(Shell.Screenshot.prototype, 'pick_color');
Gio._promisify(Shell.Screenshot.prototype, 'screenshot');
Gio._promisify(Shell.Screenshot.prototype, 'screenshot_window');
Gio._promisify(Shell.Screenshot.prototype, 'screenshot_area');
Gio._promisify(Shell.Screenshot.prototype, 'screenshot_stage_to_content');
Gio._promisify(Shell.Screenshot, 'composite_to_stream');

import {ScreencastErrors, ScreencastError} from '../misc/dbusErrors.js';
import {loadInterfaceXML} from '../misc/fileUtils.js';
import {DBusSenderChecker} from '../misc/util.js';

const ScreenshotIface = loadInterfaceXML('org.gnome.Shell.Screenshot');

const ScreencastIface = loadInterfaceXML('org.gnome.Shell.Screencast');
const ScreencastProxy = Gio.DBusProxy.makeProxyWrapper(ScreencastIface);

const IconLabelButton = GObject.registerClass(
class IconLabelButton extends St.Button {
    _init(iconName, label, params) {
        super._init(params);

        this._container = new St.BoxLayout({
            vertical: true,
            style_class: 'icon-label-button-container',
        });
        this.set_child(this._container);

        this._container.add_child(new St.Icon({icon_name: iconName}));
        this._container.add_child(new St.Label({
            text: label,
            x_align: Clutter.ActorAlign.CENTER,
        }));
    }
});

export const Tooltip = GObject.registerClass(
class Tooltip extends St.Label {
    _init(widget, params) {
        super._init(params);

        this._widget = widget;
        this._timeoutId = null;

        this._widget.connect('notify::hover', () => {
            if (this._widget.hover)
                this.open();
            else
                this.close();
        });
    }

    open() {
        if (this._timeoutId)
            return;

        this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 300, () => {
            this.opacity = 0;
            this.show();

            const extents = this._widget.get_transformed_extents();

            const xOffset = Math.floor((extents.get_width() - this.width) / 2);
            const x =
                Math.clamp(extents.get_x() + xOffset, 0, global.stage.width - this.width);

            const node = this.get_theme_node();
            const yOffset = node.get_length('-y-offset');

            const y = extents.get_y() - this.height - yOffset;

            this.set_position(x, y);
            this.ease({
                opacity: 255,
                duration: 150,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });

            this._timeoutId = null;
            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(this._timeoutId, '[gnome-shell] tooltip.open');
    }

    close() {
        if (this._timeoutId) {
            GLib.source_remove(this._timeoutId);
            this._timeoutId = null;
            return;
        }

        if (!this.visible)
            return;

        this.remove_all_transitions();
        this.ease({
            opacity: 0,
            duration: 100,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this.hide(),
        });
    }
});

const UIAreaIndicator = GObject.registerClass(
class UIAreaIndicator extends St.Widget {
    _init(params) {
        super._init(params);

        this._topRect = new St.Widget({style_class: 'screenshot-ui-area-indicator-shade'});
        this._topRect.add_constraint(new Clutter.BindConstraint({
            source: this,
            coordinate: Clutter.BindCoordinate.WIDTH,
        }));
        this._topRect.add_constraint(new Clutter.SnapConstraint({
            source: this,
            from_edge: Clutter.SnapEdge.TOP,
            to_edge: Clutter.SnapEdge.TOP,
        }));
        this._topRect.add_constraint(new Clutter.SnapConstraint({
            source: this,
            from_edge: Clutter.SnapEdge.LEFT,
            to_edge: Clutter.SnapEdge.LEFT,
        }));
        this.add_child(this._topRect);

        this._bottomRect = new St.Widget({style_class: 'screenshot-ui-area-indicator-shade'});
        this._bottomRect.add_constraint(new Clutter.BindConstraint({
            source: this,
            coordinate: Clutter.BindCoordinate.WIDTH,
        }));
        this._bottomRect.add_constraint(new Clutter.SnapConstraint({
            source: this,
            from_edge: Clutter.SnapEdge.BOTTOM,
            to_edge: Clutter.SnapEdge.BOTTOM,
        }));
        this._bottomRect.add_constraint(new Clutter.SnapConstraint({
            source: this,
            from_edge: Clutter.SnapEdge.LEFT,
            to_edge: Clutter.SnapEdge.LEFT,
        }));
        this.add_child(this._bottomRect);

        this._leftRect = new St.Widget({style_class: 'screenshot-ui-area-indicator-shade'});
        this._leftRect.add_constraint(new Clutter.SnapConstraint({
            source: this,
            from_edge: Clutter.SnapEdge.LEFT,
            to_edge: Clutter.SnapEdge.LEFT,
        }));
        this._leftRect.add_constraint(new Clutter.SnapConstraint({
            source: this._topRect,
            from_edge: Clutter.SnapEdge.TOP,
            to_edge: Clutter.SnapEdge.BOTTOM,
        }));
        this._leftRect.add_constraint(new Clutter.SnapConstraint({
            source: this._bottomRect,
            from_edge: Clutter.SnapEdge.BOTTOM,
            to_edge: Clutter.SnapEdge.TOP,
        }));
        this.add_child(this._leftRect);

        this._rightRect = new St.Widget({style_class: 'screenshot-ui-area-indicator-shade'});
        this._rightRect.add_constraint(new Clutter.SnapConstraint({
            source: this,
            from_edge: Clutter.SnapEdge.RIGHT,
            to_edge: Clutter.SnapEdge.RIGHT,
        }));
        this._rightRect.add_constraint(new Clutter.SnapConstraint({
            source: this._topRect,
            from_edge: Clutter.SnapEdge.TOP,
            to_edge: Clutter.SnapEdge.BOTTOM,
        }));
        this._rightRect.add_constraint(new Clutter.SnapConstraint({
            source: this._bottomRect,
            from_edge: Clutter.SnapEdge.BOTTOM,
            to_edge: Clutter.SnapEdge.TOP,
        }));
        this.add_child(this._rightRect);

        this._selectionRect = new St.Widget({style_class: 'screenshot-ui-area-indicator-selection'});
        this.add_child(this._selectionRect);

        this._topRect.add_constraint(new Clutter.SnapConstraint({
            source: this._selectionRect,
            from_edge: Clutter.SnapEdge.BOTTOM,
            to_edge: Clutter.SnapEdge.TOP,
        }));

        this._bottomRect.add_constraint(new Clutter.SnapConstraint({
            source: this._selectionRect,
            from_edge: Clutter.SnapEdge.TOP,
            to_edge: Clutter.SnapEdge.BOTTOM,
        }));

        this._leftRect.add_constraint(new Clutter.SnapConstraint({
            source: this._selectionRect,
            from_edge: Clutter.SnapEdge.RIGHT,
            to_edge: Clutter.SnapEdge.LEFT,
        }));

        this._rightRect.add_constraint(new Clutter.SnapConstraint({
            source: this._selectionRect,
            from_edge: Clutter.SnapEdge.LEFT,
            to_edge: Clutter.SnapEdge.RIGHT,
        }));
    }

    setSelectionRect(x, y, width, height) {
        this._selectionRect.set_position(x, y);
        this._selectionRect.set_size(width, height);
    }
});

const UIAreaSelector = GObject.registerClass({
    Signals: {'drag-started': {}, 'drag-ended': {}},
}, class UIAreaSelector extends St.Widget {
    _init(params) {
        super._init(params);

        // During a drag, this can be Clutter.BUTTON_PRIMARY,
        // Clutter.BUTTON_SECONDARY or the string "touch" to identify the source
        // of the drag operation.
        this._dragButton = 0;
        this._dragSequence = null;

        this._areaIndicator = new UIAreaIndicator();
        this._areaIndicator.add_constraint(new Clutter.BindConstraint({
            source: this,
            coordinate: Clutter.BindCoordinate.ALL,
        }));
        this.add_child(this._areaIndicator);

        this._topLeftHandle = new St.Widget({style_class: 'screenshot-ui-area-selector-handle'});
        this.add_child(this._topLeftHandle);
        this._topRightHandle = new St.Widget({style_class: 'screenshot-ui-area-selector-handle'});
        this.add_child(this._topRightHandle);
        this._bottomLeftHandle = new St.Widget({style_class: 'screenshot-ui-area-selector-handle'});
        this.add_child(this._bottomLeftHandle);
        this._bottomRightHandle = new St.Widget({style_class: 'screenshot-ui-area-selector-handle'});
        this.add_child(this._bottomRightHandle);

        // This will be updated before the first drawn frame.
        this._handleSize = 0;
        this._topLeftHandle.connect('style-changed', widget => {
            this._handleSize = widget.get_theme_node().get_width();
            this._updateSelectionRect();
        });

        this.connect('notify::mapped', () => {
            if (this.mapped) {
                const [x, y] = global.get_pointer();
                this._updateCursor(x, y);
            }
        });

        // Initialize area to out of bounds so reset() below resets it.
        this._startX = -1;
        this._startY = 0;
        this._lastX = 0;
        this._lastY = 0;

        this.reset();
    }

    reset() {
        this.stopDrag();
        global.display.set_cursor(Meta.Cursor.DEFAULT);

        // Preserve area selection if possible. If the area goes out of bounds,
        // the monitors might have changed, so reset the area.
        const [x, y, w, h] = this.getGeometry();
        if (x < 0 || y < 0 || x + w > this.width || y + h > this.height) {
            // Initialize area to out of bounds so if there's no monitor,
            // the area will be reset once a monitor does appear.
            this._startX = -1;
            this._startY = 0;
            this._lastX = 0;
            this._lastY = 0;

            // This can happen when running headless without any monitors.
            if (Main.layoutManager.primaryIndex !== -1) {
                const monitor =
                    Main.layoutManager.monitors[Main.layoutManager.primaryIndex];

                this._startX = monitor.x + Math.floor(monitor.width * 3 / 8);
                this._startY = monitor.y + Math.floor(monitor.height * 3 / 8);
                this._lastX = monitor.x + Math.floor(monitor.width * 5 / 8) - 1;
                this._lastY = monitor.y + Math.floor(monitor.height * 5 / 8) - 1;
            }

            this._updateSelectionRect();
        }
    }

    getGeometry() {
        const leftX = Math.min(this._startX, this._lastX);
        const topY = Math.min(this._startY, this._lastY);
        const rightX = Math.max(this._startX, this._lastX);
        const bottomY = Math.max(this._startY, this._lastY);

        return [leftX, topY, rightX - leftX + 1, bottomY - topY + 1];
    }

    _updateSelectionRect() {
        const [x, y, w, h] = this.getGeometry();
        this._areaIndicator.setSelectionRect(x, y, w, h);

        const offset = this._handleSize / 2;
        this._topLeftHandle.set_position(x - offset, y - offset);
        this._topRightHandle.set_position(x + w - 1 - offset, y - offset);
        this._bottomLeftHandle.set_position(x - offset, y + h - 1 - offset);
        this._bottomRightHandle.set_position(x + w - 1 - offset, y + h - 1 - offset);
    }

    _computeCursorType(cursorX, cursorY) {
        const [leftX, topY, width, height] = this.getGeometry();
        const [rightX, bottomY] = [leftX + width - 1, topY + height - 1];
        const [x, y] = [cursorX, cursorY];

        // Check if the cursor overlaps the handles first.
        const limit = (this._handleSize / 2) ** 2;
        if ((leftX - x) ** 2 + (topY - y) ** 2 <= limit)
            return Meta.Cursor.NW_RESIZE;
        else if ((rightX - x) ** 2 + (topY - y) ** 2 <= limit)
            return Meta.Cursor.NE_RESIZE;
        else if ((leftX - x) ** 2 + (bottomY - y) ** 2 <= limit)
            return Meta.Cursor.SW_RESIZE;
        else if ((rightX - x) ** 2 + (bottomY - y) ** 2 <= limit)
            return Meta.Cursor.SE_RESIZE;

        // Now check the rest of the rectangle.
        const threshold =
            10 * St.ThemeContext.get_for_stage(global.stage).scaleFactor;

        if (leftX - x >= 0 && leftX - x <= threshold) {
            if (topY - y >= 0 && topY - y <= threshold)
                return Meta.Cursor.NW_RESIZE;
            else if (y - bottomY >= 0 && y - bottomY <= threshold)
                return Meta.Cursor.SW_RESIZE;
            else if (topY - y < 0 && y - bottomY < 0)
                return Meta.Cursor.WEST_RESIZE;
        } else if (x - rightX >= 0 && x - rightX <= threshold) {
            if (topY - y >= 0 && topY - y <= threshold)
                return Meta.Cursor.NE_RESIZE;
            else if (y - bottomY >= 0 && y - bottomY <= threshold)
                return Meta.Cursor.SE_RESIZE;
            else if (topY - y < 0 && y - bottomY < 0)
                return Meta.Cursor.EAST_RESIZE;
        } else if (leftX - x < 0 && x - rightX < 0) {
            if (topY - y >= 0 && topY - y <= threshold)
                return Meta.Cursor.NORTH_RESIZE;
            else if (y - bottomY >= 0 && y - bottomY <= threshold)
                return Meta.Cursor.SOUTH_RESIZE;
            else if (topY - y < 0 && y - bottomY < 0)
                return Meta.Cursor.MOVE_OR_RESIZE_WINDOW;
        }

        return Meta.Cursor.CROSSHAIR;
    }

    stopDrag() {
        if (!this._dragButton)
            return;

        if (this._dragGrab) {
            this._dragGrab.dismiss();
            this._dragGrab = null;
        }

        this._dragButton = 0;
        this._dragSequence = null;

        if (this._dragCursor === Meta.Cursor.CROSSHAIR &&
            this._lastX === this._startX && this._lastY === this._startY) {
            // The user clicked without dragging. Make up a larger selection
            // to reduce confusion.
            const offset =
                20 * St.ThemeContext.get_for_stage(global.stage).scaleFactor;
            this._startX -= offset;
            this._startY -= offset;
            this._lastX += offset;
            this._lastY += offset;

            // Keep the coordinates inside the stage.
            if (this._startX < 0) {
                this._lastX -= this._startX;
                this._startX = 0;
            } else if (this._lastX >= this.width) {
                this._startX -= this._lastX - this.width + 1;
                this._lastX = this.width - 1;
            }

            if (this._startY < 0) {
                this._lastY -= this._startY;
                this._startY = 0;
            } else if (this._lastY >= this.height) {
                this._startY -= this._lastY - this.height + 1;
                this._lastY = this.height - 1;
            }

            this._updateSelectionRect();
        }

        this.emit('drag-ended');
    }

    _updateCursor(x, y) {
        const cursor = this._computeCursorType(x, y);
        global.display.set_cursor(cursor);
    }

    _onPress(event, button, sequence) {
        if (this._dragButton)
            return Clutter.EVENT_PROPAGATE;

        const [x, y] = event.get_coords();
        const cursor = this._computeCursorType(x, y);

        // Clicking outside of the selection, or using the right mouse button,
        // or with Ctrl results in dragging a new selection from scratch.
        if (cursor === Meta.Cursor.CROSSHAIR ||
            button === Clutter.BUTTON_SECONDARY ||
            (event.get_state() & Clutter.ModifierType.CONTROL_MASK)) {
            this._dragButton = button;

            this._dragCursor = Meta.Cursor.CROSSHAIR;
            global.display.set_cursor(Meta.Cursor.CROSSHAIR);

            [this._startX, this._startY] = event.get_coords();
            this._lastX = this._startX = Math.floor(this._startX);
            this._lastY = this._startY = Math.floor(this._startY);

            this._updateSelectionRect();
        } else {
            // This is a move or resize operation.
            this._dragButton = button;

            this._dragCursor = cursor;
            [this._dragStartX, this._dragStartY] = event.get_coords();

            const [leftX, topY, width, height] = this.getGeometry();
            const rightX = leftX + width - 1;
            const bottomY = topY + height - 1;

            // For moving, start X and Y are the top left corner, while
            // last X and Y are the bottom right corner.
            if (cursor === Meta.Cursor.MOVE_OR_RESIZE_WINDOW) {
                this._startX = leftX;
                this._startY = topY;
                this._lastX = rightX;
                this._lastY = bottomY;
            }

            // Start X and Y are set to the stationary sides, while last X
            // and Y are set to the moving sides.
            if (cursor === Meta.Cursor.NW_RESIZE ||
                cursor === Meta.Cursor.WEST_RESIZE ||
                cursor === Meta.Cursor.SW_RESIZE) {
                this._startX = rightX;
                this._lastX = leftX;
            }
            if (cursor === Meta.Cursor.NE_RESIZE ||
                cursor === Meta.Cursor.EAST_RESIZE ||
                cursor === Meta.Cursor.SE_RESIZE) {
                this._startX = leftX;
                this._lastX = rightX;
            }
            if (cursor === Meta.Cursor.NW_RESIZE ||
                cursor === Meta.Cursor.NORTH_RESIZE ||
                cursor === Meta.Cursor.NE_RESIZE) {
                this._startY = bottomY;
                this._lastY = topY;
            }
            if (cursor === Meta.Cursor.SW_RESIZE ||
                cursor === Meta.Cursor.SOUTH_RESIZE ||
                cursor === Meta.Cursor.SE_RESIZE) {
                this._startY = topY;
                this._lastY = bottomY;
            }
        }

        if (this._dragButton) {
            this._dragGrab = global.stage.grab(this);
            this._dragSequence = sequence;

            this.emit('drag-started');

            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _onRelease(event, button, sequence) {
        if (this._dragButton !== button ||
            this._dragSequence?.get_slot() !== sequence?.get_slot())
            return Clutter.EVENT_PROPAGATE;

        this.stopDrag();

        // We might have finished creating a new selection, so we need to
        // update the cursor.
        const [x, y] = event.get_coords();
        this._updateCursor(x, y);

        return Clutter.EVENT_STOP;
    }

    _onMotion(event, sequence) {
        if (!this._dragButton) {
            const [x, y] = event.get_coords();
            this._updateCursor(x, y);
            return Clutter.EVENT_PROPAGATE;
        }

        if (sequence?.get_slot() !== this._dragSequence?.get_slot())
            return Clutter.EVENT_PROPAGATE;

        if (this._dragCursor === Meta.Cursor.CROSSHAIR) {
            [this._lastX, this._lastY] = event.get_coords();
            this._lastX = Math.floor(this._lastX);
            this._lastY = Math.floor(this._lastY);
        } else {
            const [x, y] = event.get_coords();
            let dx = Math.round(x - this._dragStartX);
            let dy = Math.round(y - this._dragStartY);

            if (this._dragCursor === Meta.Cursor.MOVE_OR_RESIZE_WINDOW) {
                const [,, selectionWidth, selectionHeight] = this.getGeometry();

                let newStartX = this._startX + dx;
                let newStartY = this._startY + dy;
                let newLastX = this._lastX + dx;
                let newLastY = this._lastY + dy;

                let overshootX = 0;
                let overshootY = 0;

                // Keep the size intact if we bumped into the stage edge.
                if (newStartX < 0) {
                    overshootX = 0 - newStartX;
                    newStartX = 0;
                    newLastX = newStartX + (selectionWidth - 1);
                } else if (newLastX > this.width - 1) {
                    overshootX = (this.width - 1) - newLastX;
                    newLastX = this.width - 1;
                    newStartX = newLastX - (selectionWidth - 1);
                }

                if (newStartY < 0) {
                    overshootY = 0 - newStartY;
                    newStartY = 0;
                    newLastY = newStartY + (selectionHeight - 1);
                } else if (newLastY > this.height - 1) {
                    overshootY = (this.height - 1) - newLastY;
                    newLastY = this.height - 1;
                    newStartY = newLastY - (selectionHeight - 1);
                }

                // Add the overshoot to the delta to create a "rubberbanding"
                // behavior of the pointer when dragging.
                dx += overshootX;
                dy += overshootY;

                this._startX = newStartX;
                this._startY = newStartY;
                this._lastX = newLastX;
                this._lastY = newLastY;
            } else {
                if (this._dragCursor === Meta.Cursor.WEST_RESIZE ||
                    this._dragCursor === Meta.Cursor.EAST_RESIZE)
                    dy = 0;
                if (this._dragCursor === Meta.Cursor.NORTH_RESIZE ||
                    this._dragCursor === Meta.Cursor.SOUTH_RESIZE)
                    dx = 0;

                // Make sure last X and Y are clamped between 0 and size - 1,
                // while always preserving the cursor dragging position relative
                // to the selection rectangle.
                this._lastX += dx;
                if (this._lastX >= this.width) {
                    dx -= this._lastX - this.width + 1;
                    this._lastX = this.width - 1;
                } else if (this._lastX < 0) {
                    dx -= this._lastX;
                    this._lastX = 0;
                }

                this._lastY += dy;
                if (this._lastY >= this.height) {
                    dy -= this._lastY - this.height + 1;
                    this._lastY = this.height - 1;
                } else if (this._lastY < 0) {
                    dy -= this._lastY;
                    this._lastY = 0;
                }

                // If we drag the handle past a selection side, update which
                // handles are which.
                if (this._lastX > this._startX) {
                    if (this._dragCursor === Meta.Cursor.NW_RESIZE)
                        this._dragCursor = Meta.Cursor.NE_RESIZE;
                    else if (this._dragCursor === Meta.Cursor.SW_RESIZE)
                        this._dragCursor = Meta.Cursor.SE_RESIZE;
                    else if (this._dragCursor === Meta.Cursor.WEST_RESIZE)
                        this._dragCursor = Meta.Cursor.EAST_RESIZE;
                } else {
                    // eslint-disable-next-line no-lonely-if
                    if (this._dragCursor === Meta.Cursor.NE_RESIZE)
                        this._dragCursor = Meta.Cursor.NW_RESIZE;
                    else if (this._dragCursor === Meta.Cursor.SE_RESIZE)
                        this._dragCursor = Meta.Cursor.SW_RESIZE;
                    else if (this._dragCursor === Meta.Cursor.EAST_RESIZE)
                        this._dragCursor = Meta.Cursor.WEST_RESIZE;
                }

                if (this._lastY > this._startY) {
                    if (this._dragCursor === Meta.Cursor.NW_RESIZE)
                        this._dragCursor = Meta.Cursor.SW_RESIZE;
                    else if (this._dragCursor === Meta.Cursor.NE_RESIZE)
                        this._dragCursor = Meta.Cursor.SE_RESIZE;
                    else if (this._dragCursor === Meta.Cursor.NORTH_RESIZE)
                        this._dragCursor = Meta.Cursor.SOUTH_RESIZE;
                } else {
                    // eslint-disable-next-line no-lonely-if
                    if (this._dragCursor === Meta.Cursor.SW_RESIZE)
                        this._dragCursor = Meta.Cursor.NW_RESIZE;
                    else if (this._dragCursor === Meta.Cursor.SE_RESIZE)
                        this._dragCursor = Meta.Cursor.NE_RESIZE;
                    else if (this._dragCursor === Meta.Cursor.SOUTH_RESIZE)
                        this._dragCursor = Meta.Cursor.NORTH_RESIZE;
                }

                global.display.set_cursor(this._dragCursor);
            }

            this._dragStartX += dx;
            this._dragStartY += dy;
        }

        this._updateSelectionRect();

        return Clutter.EVENT_STOP;
    }

    vfunc_button_press_event(event) {
        const button = event.get_button();
        if (button === Clutter.BUTTON_PRIMARY ||
            button === Clutter.BUTTON_SECONDARY)
            return this._onPress(event, button, null);

        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_button_release_event(event) {
        const button = event.get_button();
        if (button === Clutter.BUTTON_PRIMARY ||
            button === Clutter.BUTTON_SECONDARY)
            return this._onRelease(event, button, null);

        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_motion_event(event) {
        return this._onMotion(event, null);
    }

    vfunc_touch_event(event) {
        const eventType = event.type();
        if (eventType === Clutter.EventType.TOUCH_BEGIN)
            return this._onPress(event, 'touch', event.get_event_sequence());
        else if (eventType === Clutter.EventType.TOUCH_END)
            return this._onRelease(event, 'touch', event.get_event_sequence());
        else if (eventType === Clutter.EventType.TOUCH_UPDATE)
            return this._onMotion(event, event.get_event_sequence());

        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_leave_event(event) {
        // If we're dragging and go over the panel we still get a leave event
        // for some reason, even though we have a grab. We don't want to switch
        // the cursor when we're dragging.
        if (!this._dragButton)
            global.display.set_cursor(Meta.Cursor.DEFAULT);

        return super.vfunc_leave_event(event);
    }
});

const UIWindowSelectorLayout = GObject.registerClass(
class UIWindowSelectorLayout extends Workspace.WorkspaceLayout {
    _init(monitorIndex) {
        super._init(null, monitorIndex, null);
    }

    vfunc_set_container(container) {
        this._container = container;
        this._syncWorkareaTracking();
    }

    vfunc_allocate(container, box) {
        const containerBox = container.allocation;
        const containerAllocationChanged =
            this._lastBox === null || !this._lastBox.equal(containerBox);
        this._lastBox = containerBox.copy();

        let layoutChanged = false;
        if (this._layout === null) {
            this._layout = this._createBestLayout(this._workarea);
            layoutChanged = true;
        }

        if (layoutChanged || containerAllocationChanged)
            this._windowSlots = this._getWindowSlots(box.copy());

        const childBox = new Clutter.ActorBox();

        const nSlots = this._windowSlots.length;
        for (let i = 0; i < nSlots; i++) {
            let [x, y, width, height, child] = this._windowSlots[i];

            childBox.set_origin(x, y);
            childBox.set_size(width, height);

            child.allocate(childBox);
        }
    }

    addWindow(window) {
        if (this._sortedWindows.includes(window))
            return;

        this._sortedWindows.push(window);

        this._container.add_child(window);

        this._layout = null;
        this.layout_changed();
    }

    reset() {
        for (const window of this._sortedWindows)
            window.destroy();

        this._sortedWindows = [];
        this._windowSlots = [];
        this._layout = null;
    }

    get windows() {
        return this._sortedWindows;
    }
});

const UIWindowSelectorWindowContent = GObject.registerClass(
class UIWindowSelectorWindowContent extends Clutter.Actor {
    constructor(actor) {
        super({
            x_expand: true,
            y_expand: true,
        });

        const window = actor.metaWindow;
        this._boundingBox = window.get_frame_rect();
        this._bufferRect = window.get_buffer_rect();
        this._bufferScale = actor.get_resource_scale();
        this._actor = new Clutter.Actor({
            content: actor.paint_to_content(null),
        });
        this.add_child(this._actor);

        this._border = new St.Bin({
            style_class: 'screenshot-ui-window-selector-window-border',
            child: new St.Icon({
                icon_name: 'object-select-symbolic',
                style_class: 'screenshot-ui-window-selector-check',
                x_align: Clutter.ActorAlign.CENTER,
                y_align: Clutter.ActorAlign.CENTER,
            }),
        });
        this._border.connect('style-changed', () => {
            this._borderSize =
                this._border.get_theme_node().get_border_width(St.Side.TOP);
        });
        this.add_child(this._border);

        this._cursor = null;
        this._cursorPoint = {x: 0, y: 0};
        this._shouldShowCursor = window.has_pointer && window.has_pointer();

        this.connect('destroy', this._onDestroy.bind(this));
    }

    get boundingBox() {
        return this._boundingBox;
    }

    get cursorPoint() {
        return {
            x: this._cursorPoint.x + this._boundingBox.x - this._bufferRect.x,
            y: this._cursorPoint.y + this._boundingBox.y - this._bufferRect.y,
        };
    }

    get bufferScale() {
        return this._bufferScale;
    }

    get windowContent() {
        return this._actor.content;
    }

    _onDestroy() {
        this.remove_child(this._actor);
        this._actor.destroy();
        this._actor = null;
        this.remove_child(this._border);
        this._border.destroy();
        this._border = null;

        if (this._cursor) {
            this.remove_child(this._cursor);
            this._cursor.destroy();
            this._cursor = null;
        }
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        // Border goes around the window.
        const borderBox = box.copy();
        borderBox.set_origin(0, 0);
        borderBox.x1 -= this._borderSize;
        borderBox.y1 -= this._borderSize;
        borderBox.x2 += this._borderSize;
        borderBox.y2 += this._borderSize;
        this._border.allocate(borderBox);

        // box should contain this._boundingBox worth of window. Compute
        // origin and size for the actor box to satisfy that.
        const xScale = box.get_width() / this._boundingBox.width;
        const yScale = box.get_height() / this._boundingBox.height;

        const [, windowW, windowH] = this._actor.content.get_preferred_size();

        const actorBox = new Clutter.ActorBox();
        actorBox.set_origin(
            (this._bufferRect.x - this._boundingBox.x) * xScale,
            (this._bufferRect.y - this._boundingBox.y) * yScale
        );
        actorBox.set_size(
            windowW * xScale / this._bufferScale,
            windowH * yScale / this._bufferScale
        );
        this._actor.allocate(actorBox);

        // Allocate the cursor if we have one.
        if (!this._cursor)
            return;

        let [, , w, h] = this._cursor.get_preferred_size();
        w *= this._cursorScale;
        h *= this._cursorScale;

        const cursorBox = new Clutter.ActorBox({
            x1: this._cursorPoint.x,
            y1: this._cursorPoint.y,
            x2: this._cursorPoint.x + w,
            y2: this._cursorPoint.y + h,
        });
        cursorBox.x1 *= xScale;
        cursorBox.x2 *= xScale;
        cursorBox.y1 *= yScale;
        cursorBox.y2 *= yScale;

        this._cursor.allocate(cursorBox);
    }

    addCursorTexture(content, point, scale) {
        if (!this._shouldShowCursor)
            return;

        // Add the cursor.
        this._cursor = new St.Widget({
            content,
            request_mode: Clutter.RequestMode.CONTENT_SIZE,
        });

        this._cursorPoint = {
            x: point.x - this._boundingBox.x,
            y: point.y - this._boundingBox.y,
        };
        this._cursorScale = scale;

        this.insert_child_below(this._cursor, this._border);
    }

    getCursorTexture() {
        return this._cursor?.content;
    }

    setCursorVisible(visible) {
        if (!this._cursor)
            return;

        this._cursor.visible = visible;
    }
});

const UIWindowSelectorWindow = GObject.registerClass(
class UIWindowSelectorWindow extends St.Button {
    _init(actor, params) {
        super._init({
            child: new UIWindowSelectorWindowContent(actor),
            ...params,
        });
    }

    get boundingBox() {
        return this.child.boundingBox;
    }

    get windowCenter() {
        const boundingBox = this.boundingBox;
        return {
            x: boundingBox.x + boundingBox.width / 2,
            y: boundingBox.y + boundingBox.height / 2,
        };
    }

    chromeHeights() {
        return [0, 0];
    }

    chromeWidths() {
        return [0, 0];
    }

    overlapHeights() {
        return [0, 0];
    }

    get cursorPoint() {
        return this.child.cursorPoint;
    }

    get bufferScale() {
        return this.child.bufferScale;
    }

    get windowContent() {
        return this.child.windowContent;
    }

    addCursorTexture(content, point, scale) {
        this.child.addCursorTexture(content, point, scale);
    }

    getCursorTexture() {
        return this.child.getCursorTexture();
    }

    setCursorVisible(visible) {
        this.child.setCursorVisible(visible);
    }
});

const UIWindowSelector = GObject.registerClass(
class UIWindowSelector extends St.Widget {
    _init(monitorIndex, params) {
        super._init(params);
        super.layout_manager = new Clutter.BinLayout();

        this._monitorIndex = monitorIndex;

        this._layoutManager = new UIWindowSelectorLayout(monitorIndex);

        // Window screenshots
        this._container = new St.Widget({
            style_class: 'screenshot-ui-window-selector-window-container',
            x_expand: true,
            y_expand: true,
        });
        this._container.layout_manager = this._layoutManager;
        this.add_child(this._container);
    }

    capture() {
        for (const actor of global.get_window_actors()) {
            let window = actor.metaWindow;
            let workspaceManager = global.workspace_manager;
            let activeWorkspace = workspaceManager.get_active_workspace();
            if (window.is_override_redirect() ||
                !window.located_on_workspace(activeWorkspace) ||
                window.get_monitor() !== this._monitorIndex)
                continue;

            const widget = new UIWindowSelectorWindow(
                actor,
                {
                    style_class: 'screenshot-ui-window-selector-window',
                    reactive: true,
                    can_focus: true,
                    toggle_mode: true,
                }
            );

            widget.connect('key-focus-in', win => {
                Main.screenshotUI.grab_key_focus();
                win.checked = true;
            });

            if (window.has_focus()) {
                widget.checked = true;
                widget.toggle_mode = false;
            }

            this._layoutManager.addWindow(widget);
        }
    }

    reset() {
        this._layoutManager.reset();
    }

    windows() {
        return this._layoutManager.windows;
    }
});

const UIMode = {
    SCREENSHOT: 0,
    SCREENCAST: 1,
    SCREENSHOT_ONLY: 2,
};

const ScreencastPhase = {
    STARTUP: 'STARTUP',
    RECORDING: 'RECORDING',
};

export const ScreenshotUI = GObject.registerClass({
    Properties: {
        'screencast-in-progress': GObject.ParamSpec.boolean(
            'screencast-in-progress',
            'screencast-in-progress',
            'screencast-in-progress',
            GObject.ParamFlags.READABLE,
            false),
    },
    Signals: {
        'screenshot-taken': {param_types: [Gio.File.$gtype]},
        'closed': {},
    },
}, class ScreenshotUI extends St.Widget {
    _init() {
        super._init({
            name: 'screenshot-ui',
            constraints: new Clutter.BindConstraint({
                source: global.stage,
                coordinate: Clutter.BindCoordinate.ALL,
            }),
            layout_manager: new Clutter.BinLayout(),
            opacity: 0,
            visible: false,
            reactive: true,
        });

        this._screencastInProgress = false;
        this._screencastSupported = false;
        this._currentMode = UIMode.SCREENSHOT;

        this._screencastProxy = new ScreencastProxy(
            Gio.DBus.session,
            'org.gnome.Shell.Screencast',
            '/org/gnome/Shell/Screencast',
            (object, error) => {
                if (error !== null) {
                    log('Error connecting to the screencast service');
                    return;
                }

                this._screencastSupported = this._screencastProxy.ScreencastSupported;
                this._syncCastButton();
            });

        this._screencastProxy.connectSignal('Error', (proxy, sender, [errorName, message]) =>
            this._screencastFailed(ScreencastPhase.RECORDING,
                Gio.DBusError.new_for_dbus_error(errorName, message)));

        this._screencastProxy.connect('notify::g-name-owner', () => {
            if (this._screencastProxy.g_name_owner)
                return;

            if (!this._screencastInProgress)
                return;

            // If the recorder crashed while we're starting it in _startScreencast(),
            // let the catch-block there handle the error.
            if (this._screencastStarting)
                return;

            this._screencastFailed(ScreencastPhase.RECORDING,
                new GLib.Error(ScreencastErrors, ScreencastError.SERVICE_CRASH,
                    'Service crashed'));
        });

        this._lockdownSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.lockdown'});

        // The full-screen screenshot has a separate container so that we can
        // show it without the screenshot UI fade-in for a nicer animation.
        this._stageScreenshotContainer = new St.Widget({visible: false});
        this._stageScreenshotContainer.add_constraint(new Clutter.BindConstraint({
            source: global.stage,
            coordinate: Clutter.BindCoordinate.ALL,
        }));
        Main.layoutManager.screenshotUIGroup.add_child(
            this._stageScreenshotContainer);

        this._screencastAreaIndicator = new UIAreaIndicator({
            style_class: 'screenshot-ui-screencast-area-indicator',
            visible: false,
        });
        this._screencastAreaIndicator.add_constraint(new Clutter.BindConstraint({
            source: global.stage,
            coordinate: Clutter.BindCoordinate.ALL,
        }));
        this.bind_property(
            'screencast-in-progress',
            this._screencastAreaIndicator,
            'visible',
            GObject.BindingFlags.DEFAULT);
        // Add it directly to the stage so that it's above popup menus.
        global.stage.add_child(this._screencastAreaIndicator);
        Shell.util_set_hidden_from_pick(this._screencastAreaIndicator, true);

        Main.layoutManager.screenshotUIGroup.add_child(this);

        this._stageScreenshot = new St.Widget({style_class: 'screenshot-ui-screen-screenshot'});
        this._stageScreenshot.add_constraint(new Clutter.BindConstraint({
            source: global.stage,
            coordinate: Clutter.BindCoordinate.ALL,
        }));
        this._stageScreenshotContainer.add_child(this._stageScreenshot);

        this._cursor = new St.Widget();
        this._stageScreenshotContainer.add_child(this._cursor);

        this._openingCoroutineInProgress = false;
        this._grabHelper = new GrabHelper.GrabHelper(this, {
            actionMode: Shell.ActionMode.POPUP,
        });

        this._areaSelector = new UIAreaSelector({
            style_class: 'screenshot-ui-area-selector',
            x_expand: true,
            y_expand: true,
            reactive: true,
        });
        this.add_child(this._areaSelector);

        this._primaryMonitorBin = new St.Widget({layout_manager: new Clutter.BinLayout()});
        this._primaryMonitorBin.add_constraint(
            new Layout.MonitorConstraint({'primary': true}));
        this.add_child(this._primaryMonitorBin);

        this._panel = new St.BoxLayout({
            style_class: 'screenshot-ui-panel',
            y_align: Clutter.ActorAlign.END,
            y_expand: true,
            vertical: true,
            offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY,
        });
        this._primaryMonitorBin.add_child(this._panel);

        this._closeButton = new St.Button({
            style_class: 'screenshot-ui-close-button',
            icon_name: 'preview-close-symbolic',
        });
        this._closeButton.add_constraint(new Clutter.BindConstraint({
            source: this._panel,
            coordinate: Clutter.BindCoordinate.POSITION,
        }));
        this._closeButton.add_constraint(new Clutter.AlignConstraint({
            source: this._panel,
            align_axis: Clutter.AlignAxis.Y_AXIS,
            pivot_point: new Graphene.Point({x: -1, y: 0.5}),
            factor: 0,
        }));
        this._closeButtonXAlignConstraint = new Clutter.AlignConstraint({
            source: this._panel,
            align_axis: Clutter.AlignAxis.X_AXIS,
            pivot_point: new Graphene.Point({x: 0.5, y: -1}),
        });
        this._closeButton.add_constraint(this._closeButtonXAlignConstraint);
        this._closeButton.connect('clicked', () => this.close());
        this._primaryMonitorBin.add_child(this._closeButton);

        this._areaSelector.connect('drag-started', () => {
            this._panel.ease({
                opacity: 100,
                duration: 200,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
            this._closeButton.ease({
                opacity: 100,
                duration: 200,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        });
        this._areaSelector.connect('drag-ended', () => {
            this._panel.ease({
                opacity: 255,
                duration: 200,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
            this._closeButton.ease({
                opacity: 255,
                duration: 200,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        });

        this._typeButtonContainer = new St.Widget({
            style_class: 'screenshot-ui-type-button-container',
            layout_manager: new Clutter.BoxLayout({
                spacing: 12,
                homogeneous: true,
            }),
        });
        this._panel.add_child(this._typeButtonContainer);

        this._selectionButton = new IconLabelButton('screenshot-ui-area-symbolic', _('Selection'), {
            style_class: 'screenshot-ui-type-button',
            checked: true,
            x_expand: true,
        });
        this._selectionButton.connect('notify::checked',
            this._onSelectionButtonToggled.bind(this));
        this._typeButtonContainer.add_child(this._selectionButton);

        this.add_child(new Tooltip(this._selectionButton, {
            text: _('Area Selection'),
            style_class: 'screenshot-ui-tooltip',
            visible: false,
        }));

        this._screenButton = new IconLabelButton('screenshot-ui-display-symbolic', _('Screen'), {
            style_class: 'screenshot-ui-type-button',
            toggle_mode: true,
            x_expand: true,
        });
        this._screenButton.connect('notify::checked',
            this._onScreenButtonToggled.bind(this));
        this._typeButtonContainer.add_child(this._screenButton);

        this.add_child(new Tooltip(this._screenButton, {
            text: _('Screen Selection'),
            style_class: 'screenshot-ui-tooltip',
            visible: false,
        }));

        this._windowButton = new IconLabelButton('screenshot-ui-window-symbolic', _('Window'), {
            style_class: 'screenshot-ui-type-button',
            toggle_mode: true,
            x_expand: true,
        });
        this._windowButton.connect('notify::checked',
            this._onWindowButtonToggled.bind(this));
        this._typeButtonContainer.add_child(this._windowButton);

        this.add_child(new Tooltip(this._windowButton, {
            text: _('Window Selection'),
            style_class: 'screenshot-ui-tooltip',
            visible: false,
        }));

        this._bottomRowContainer = new St.Widget({layout_manager: new Clutter.BinLayout()});
        this._panel.add_child(this._bottomRowContainer);

        this._shotCastContainer = new St.BoxLayout({
            style_class: 'screenshot-ui-shot-cast-container',
            x_align: Clutter.ActorAlign.START,
            x_expand: true,
        });
        this._bottomRowContainer.add_child(this._shotCastContainer);

        this._shotButton = new St.Button({
            style_class: 'screenshot-ui-shot-cast-button',
            icon_name: 'camera-photo-symbolic',
            checked: true,
        });
        this._shotButton.connect('notify::checked',
            this._onShotButtonToggled.bind(this));
        this._shotCastContainer.add_child(this._shotButton);

        this.add_child(new Tooltip(this._shotButton, {
            text: _('Take Screenshot'),
            style_class: 'screenshot-ui-tooltip',
            visible: false,
        }));

        this._castButton = new St.Button({
            style_class: 'screenshot-ui-shot-cast-button',
            icon_name: 'camera-web-symbolic',
            toggle_mode: true,
            visible: false,
        });
        this._castButton.connect('notify::checked',
            this._onCastButtonToggled.bind(this));
        this._shotCastContainer.add_child(this._castButton);

        this.add_child(new Tooltip(this._castButton, {
            text: _('Record Screen'),
            style_class: 'screenshot-ui-tooltip',
            visible: false,
        }));

        this._shotButton.bind_property('checked', this._castButton, 'checked',
            GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.INVERT_BOOLEAN);

        this._captureButton = new St.Button({style_class: 'screenshot-ui-capture-button'});
        this._captureButton.set_child(new St.Widget({
            style_class: 'screenshot-ui-capture-button-circle',
            x_expand: true,
            y_expand: true,
        }));
        this.add_child(new Tooltip(this._captureButton, {
            /* Translators: since this string refers to an action,
            it needs to be phrased as a verb. */
            text: _('Capture'),
            style_class: 'screenshot-ui-tooltip',
            visible: false,
        }));
        this._captureButton.connect('clicked',
            this._onCaptureButtonClicked.bind(this));
        this._bottomRowContainer.add_child(this._captureButton);

        this._showPointerButtonContainer = new St.BoxLayout({
            x_align: Clutter.ActorAlign.END,
            x_expand: true,
        });
        this._bottomRowContainer.add_child(this._showPointerButtonContainer);

        this._showPointerButton = new St.Button({
            style_class: 'screenshot-ui-show-pointer-button',
            icon_name: 'screenshot-ui-show-pointer-symbolic',
            toggle_mode: true,
        });
        this._showPointerButtonContainer.add_child(this._showPointerButton);

        this.add_child(new Tooltip(this._showPointerButton, {
            text: _('Show Pointer'),
            style_class: 'screenshot-ui-tooltip',
            visible: false,
        }));

        this._showPointerButton.connect('notify::checked', () => {
            const state = this._showPointerButton.checked;
            this._cursor.visible = state;

            const windows =
                this._windowSelectors.flatMap(selector => selector.windows());
            for (const window of windows)
                window.setCursorVisible(state);
        });
        this._cursor.visible = false;

        this._monitorBins = [];
        this._windowSelectors = [];
        this._rebuildMonitorBins();

        Main.layoutManager.connect('monitors-changed', () => {
            // Nope, not dealing with monitor changes.
            this.close(true);
            this._rebuildMonitorBins();
        });

        const uiModes =
            Shell.ActionMode.ALL & ~Shell.ActionMode.LOGIN_SCREEN;
        const restrictedModes =
            uiModes &
            ~(Shell.ActionMode.LOCK_SCREEN | Shell.ActionMode.UNLOCK_SCREEN);

        Main.wm.addKeybinding(
            'show-screenshot-ui',
            new Gio.Settings({schema_id: 'org.gnome.shell.keybindings'}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            uiModes,
            showScreenshotUI
        );

        Main.wm.addKeybinding(
            'show-screen-recording-ui',
            new Gio.Settings({schema_id: 'org.gnome.shell.keybindings'}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            restrictedModes,
            showScreenRecordingUI
        );

        Main.wm.addKeybinding(
            'screenshot-window',
            new Gio.Settings({schema_id: 'org.gnome.shell.keybindings'}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT | Meta.KeyBindingFlags.PER_WINDOW,
            restrictedModes,
            async (_display, window, _binding) => {
                try {
                    const actor = window.get_compositor_private();
                    const content = actor.paint_to_content(null);
                    const texture = content.get_texture();

                    await captureScreenshot(texture, null, 1, null);
                } catch (e) {
                    logError(e, 'Error capturing screenshot');
                }
            }
        );

        Main.wm.addKeybinding(
            'screenshot',
            new Gio.Settings({schema_id: 'org.gnome.shell.keybindings'}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            uiModes,
            async () => {
                try {
                    const shooter = new Shell.Screenshot();
                    const [content] = await shooter.screenshot_stage_to_content();
                    const texture = content.get_texture();

                    await captureScreenshot(texture, null, 1, null);
                } catch (e) {
                    logError(e, 'Error capturing screenshot');
                }
            }
        );

        Main.sessionMode.connect('updated',
            () => this._sessionUpdated());
        this._sessionUpdated();
    }

    _sessionUpdated() {
        this.close(true);
    }

    _syncCastButton() {
        const visible = this._screencastSupported;
        const reactive = visible &&
            this._currentMode !== UIMode.SCREENSHOT_ONLY &&
            Main.sessionMode.allowScreencast;

        this._castButton.set({visible, reactive});
    }

    _syncWindowButtonSensitivity() {
        const windows =
            this._windowSelectors.flatMap(selector => selector.windows());

        this._windowButton.reactive =
            Main.sessionMode.hasWindows &&
            windows.length > 0 &&
            !this._castButton.checked;
    }

    _refreshButtonLayout() {
        const buttonLayout = Meta.prefs_get_button_layout();

        this._closeButton.remove_style_class_name('left');
        this._closeButton.remove_style_class_name('right');

        if (buttonLayout.left_buttons.includes(Meta.ButtonFunction.CLOSE)) {
            this._closeButton.add_style_class_name('left');
            this._closeButtonXAlignConstraint.factor = 0;
        } else {
            this._closeButton.add_style_class_name('right');
            this._closeButtonXAlignConstraint.factor = 1;
        }
    }

    _rebuildMonitorBins() {
        for (const bin of this._monitorBins)
            bin.destroy();

        this._monitorBins = [];
        this._windowSelectors = [];
        this._screenSelectors = [];

        for (let i = 0; i < Main.layoutManager.monitors.length; i++) {
            const bin = new St.Widget({
                layout_manager: new Clutter.BinLayout(),
            });
            bin.add_constraint(new Layout.MonitorConstraint({'index': i}));
            this.insert_child_below(bin, this._primaryMonitorBin);
            this._monitorBins.push(bin);

            const windowSelector = new UIWindowSelector(i, {
                style_class: 'screenshot-ui-window-selector',
                x_expand: true,
                y_expand: true,
                visible: this._windowButton.checked,
            });
            if (i === Main.layoutManager.primaryIndex)
                windowSelector.add_style_pseudo_class('primary-monitor');

            bin.add_child(windowSelector);
            this._windowSelectors.push(windowSelector);

            const screenSelector = new St.Button({
                style_class: 'screenshot-ui-screen-selector',
                x_expand: true,
                y_expand: true,
                visible: this._screenButton.checked,
                reactive: true,
                can_focus: true,
                toggle_mode: true,
            });
            screenSelector.connect('key-focus-in', () => {
                this.grab_key_focus();
                screenSelector.checked = true;
            });
            bin.add_child(screenSelector);
            this._screenSelectors.push(screenSelector);

            screenSelector.connect('notify::checked', () => {
                if (!screenSelector.checked)
                    return;

                screenSelector.toggle_mode = false;

                for (const otherSelector of this._screenSelectors) {
                    if (screenSelector === otherSelector)
                        continue;

                    otherSelector.toggle_mode = true;
                    otherSelector.checked = false;
                }
            });
        }

        if (Main.layoutManager.primaryIndex !== -1)
            this._screenSelectors[Main.layoutManager.primaryIndex].checked = true;
    }

    async open(mode = UIMode.SCREENSHOT) {
        if (this._openingCoroutineInProgress)
            return;

        if (this._screencastInProgress)
            return;

        if (mode === UIMode.SCREENCAST && !this._screencastSupported)
            return;

        this._currentMode = mode;
        this._castButton.checked = mode === UIMode.SCREENCAST;
        this._syncCastButton();

        if (!this.visible) {
            // Screenshot UI is opening from completely closed state
            // (rather than opening back from in process of closing).
            for (const selector of this._windowSelectors)
                selector.capture();

            const windows =
                this._windowSelectors.flatMap(selector => selector.windows());
            for (const window of windows) {
                window.connect('notify::checked', () => {
                    if (!window.checked)
                        return;

                    window.toggle_mode = false;

                    for (const otherWindow of windows) {
                        if (window === otherWindow)
                            continue;

                        otherWindow.toggle_mode = true;
                        otherWindow.checked = false;
                    }
                });
            }

            this._syncWindowButtonSensitivity();
            if (!this._windowButton.reactive)
                this._selectionButton.checked = true;

            this._shooter = new Shell.Screenshot();

            this._openingCoroutineInProgress = true;
            try {
                const [content, scale, cursorContent, cursorPoint, cursorScale] =
                    await this._shooter.screenshot_stage_to_content();
                this._stageScreenshot.set_content(content);
                this._scale = scale;

                if (cursorContent !== null) {
                    this._cursor.set_content(cursorContent);
                    this._cursor.set_position(cursorPoint.x, cursorPoint.y);

                    let [, w, h] = cursorContent.get_preferred_size();
                    w *= cursorScale;
                    h *= cursorScale;
                    this._cursor.set_size(w, h);

                    this._cursorScale = cursorScale;

                    for (const window of windows) {
                        window.addCursorTexture(cursorContent, cursorPoint, cursorScale);
                        window.setCursorVisible(this._showPointerButton.checked);
                    }
                }

                this._stageScreenshotContainer.show();
            } catch (e) {
                log(`Error capturing screenshot: ${e.message}`);
            }
            this._openingCoroutineInProgress = false;
        }

        // Get rid of any popup menus.
        // We already have them captured on the screenshot anyway.
        //
        // This needs to happen before the grab below as closing menus will
        // pop their grabs.
        Main.layoutManager.emit('system-modal-opened');

        const {screenshotUIGroup} = Main.layoutManager;
        screenshotUIGroup.get_parent().set_child_above_sibling(
            screenshotUIGroup, null);

        const grabResult = this._grabHelper.grab({
            actor: this,
            onUngrab: () => this.close(),
        });
        if (!grabResult) {
            this.close(true);
            return;
        }

        this._refreshButtonLayout();

        this.remove_all_transitions();
        this.visible = true;
        this.ease({
            opacity: 255,
            duration: 200,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this._stageScreenshotContainer.get_parent().remove_child(
                    this._stageScreenshotContainer);
                this.insert_child_at_index(this._stageScreenshotContainer, 0);
            },
        });
    }

    _finishClosing() {
        this.hide();

        this._shooter = null;

        // Switch back to screenshot mode.
        this._shotButton.checked = true;

        this._stageScreenshotContainer.get_parent().remove_child(
            this._stageScreenshotContainer);
        Main.layoutManager.screenshotUIGroup.insert_child_at_index(
            this._stageScreenshotContainer, 0);
        this._stageScreenshotContainer.hide();

        this._stageScreenshot.set_content(null);
        this._cursor.set_content(null);

        this._areaSelector.reset();
        for (const selector of this._windowSelectors)
            selector.reset();

        this.emit('closed');
    }

    close(instantly = false) {
        this._grabHelper.ungrab();

        if (instantly) {
            this._finishClosing();
            return;
        }

        this.remove_all_transitions();
        this.ease({
            opacity: 0,
            duration: 200,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: this._finishClosing.bind(this),
        });
    }

    _onSelectionButtonToggled() {
        if (this._selectionButton.checked) {
            this._selectionButton.toggle_mode = false;
            this._windowButton.checked = false;
            this._screenButton.checked = false;

            this._areaSelector.show();
            this._areaSelector.remove_all_transitions();
            this._areaSelector.reactive = true;
            this._areaSelector.ease({
                opacity: 255,
                duration: 200,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        } else {
            this._selectionButton.toggle_mode = true;

            this._areaSelector.stopDrag();
            global.display.set_cursor(Meta.Cursor.DEFAULT);

            this._areaSelector.remove_all_transitions();
            this._areaSelector.reactive = false;
            this._areaSelector.ease({
                opacity: 0,
                duration: 200,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => this._areaSelector.hide(),
            });
        }
    }

    _onScreenButtonToggled() {
        if (this._screenButton.checked) {
            this._screenButton.toggle_mode = false;
            this._selectionButton.checked = false;
            this._windowButton.checked = false;

            for (const selector of this._screenSelectors) {
                selector.show();
                selector.remove_all_transitions();
                selector.ease({
                    opacity: 255,
                    duration: 200,
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                });
            }
        } else {
            this._screenButton.toggle_mode = true;

            for (const selector of this._screenSelectors) {
                selector.remove_all_transitions();
                selector.ease({
                    opacity: 0,
                    duration: 200,
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                    onComplete: () => selector.hide(),
                });
            }
        }
    }

    _onWindowButtonToggled() {
        if (this._windowButton.checked) {
            this._windowButton.toggle_mode = false;
            this._selectionButton.checked = false;
            this._screenButton.checked = false;

            for (const selector of this._windowSelectors) {
                selector.show();
                selector.remove_all_transitions();
                selector.ease({
                    opacity: 255,
                    duration: 200,
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                });
            }
        } else {
            this._windowButton.toggle_mode = true;

            for (const selector of this._windowSelectors) {
                selector.remove_all_transitions();
                selector.ease({
                    opacity: 0,
                    duration: 200,
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                    onComplete: () => selector.hide(),
                });
            }
        }
    }

    _onShotButtonToggled() {
        if (this._shotButton.checked) {
            this._shotButton.toggle_mode = false;

            this._stageScreenshotContainer.show();
            this._stageScreenshotContainer.remove_all_transitions();
            this._stageScreenshotContainer.ease({
                opacity: 255,
                duration: 200,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        } else {
            this._shotButton.toggle_mode = true;
        }
    }

    _onCastButtonToggled() {
        if (this._castButton.checked) {
            this._castButton.toggle_mode = false;

            this._captureButton.add_style_pseudo_class('cast');

            this._stageScreenshotContainer.remove_all_transitions();
            this._stageScreenshotContainer.ease({
                opacity: 0,
                duration: 200,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => this._stageScreenshotContainer.hide(),
            });

            // Screen recording doesn't support window selection yet.
            if (this._windowButton.checked)
                this._selectionButton.checked = true;

            this._windowButton.reactive = false;
        } else {
            this._castButton.toggle_mode = true;

            this._captureButton.remove_style_pseudo_class('cast');

            this._syncWindowButtonSensitivity();
        }
    }

    _getSelectedGeometry(rescale) {
        let x, y, w, h;

        if (this._selectionButton.checked) {
            [x, y, w, h] = this._areaSelector.getGeometry();
        } else if (this._screenButton.checked) {
            const index =
                this._screenSelectors.findIndex(screen => screen.checked);
            const monitor = Main.layoutManager.monitors[index];

            x = monitor.x;
            y = monitor.y;
            w = monitor.width;
            h = monitor.height;
        }

        if (rescale) {
            x *= this._scale;
            y *= this._scale;
            w *= this._scale;
            h *= this._scale;
        }

        return [x, y, w, h];
    }

    _onCaptureButtonClicked() {
        if (this._shotButton.checked) {
            this._saveScreenshot().catch(logError);
            this.close();
        } else {
            // Screencast closes the UI on its own.
            this._startScreencast();
        }
    }

    async _saveScreenshot() {
        let file = null;

        if (this._selectionButton.checked || this._screenButton.checked) {
            const content = this._stageScreenshot.get_content();
            if (!content)
                return; // Failed to capture the screenshot for some reason.

            const texture = content.get_texture();
            const geometry = this._getSelectedGeometry(true);

            let cursorTexture = this._cursor.content?.get_texture();
            if (!this._cursor.visible)
                cursorTexture = null;

            try {
                file = await captureScreenshot(
                    texture, geometry, this._scale,
                    {
                        texture: cursorTexture ?? null,
                        x: this._cursor.x * this._scale,
                        y: this._cursor.y * this._scale,
                        scale: this._cursorScale,
                    }
                );
            } catch (e) {
                logError(e, 'Error capturing screenshot');
            }
        } else if (this._windowButton.checked) {
            const window =
                this._windowSelectors.flatMap(selector => selector.windows())
                                     .find(win => win.checked);
            if (!window)
                return;

            const content = window.windowContent;
            if (!content)
                return;

            const texture = content.get_texture();

            let cursorTexture = window.getCursorTexture()?.get_texture();
            if (!this._cursor.visible)
                cursorTexture = null;

            try {
                file = await captureScreenshot(
                    texture,
                    null,
                    window.bufferScale,
                    {
                        texture: cursorTexture ?? null,
                        x: window.cursorPoint.x * window.bufferScale,
                        y: window.cursorPoint.y * window.bufferScale,
                        scale: this._cursorScale,
                    }
                );
            } catch (e) {
                logError(e, 'Error capturing screenshot');
            }
        }

        if (file)
            this.emit('screenshot-taken', file);
    }

    async _startScreencast(nRetries = 0) {
        if (this._windowButton.checked)
            return; // TODO

        const [x, y, w, h] = this._getSelectedGeometry(false);
        const drawCursor = this._cursor.visible;

        // Set up the screencast indicator rect.
        if (this._selectionButton.checked) {
            this._screencastAreaIndicator.setSelectionRect(
                ...this._areaSelector.getGeometry());
        } else if (this._screenButton.checked) {
            const index =
                this._screenSelectors.findIndex(screen => screen.checked);
            const monitor = Main.layoutManager.monitors[index];

            this._screencastAreaIndicator.setSelectionRect(
                monitor.x, monitor.y, monitor.width, monitor.height);
        }

        // Close instantly so the fade-out doesn't get recorded.
        this.close(true);

        // This is a bit awkward because creating a proxy synchronously hangs Shell.
        let method =
            this._screencastProxy.ScreencastAsync.bind(this._screencastProxy);
        if (w !== -1) {
            method = this._screencastProxy.ScreencastAreaAsync.bind(
                this._screencastProxy, x, y, w, h);
        }

        // Set this before calling the method as the screen recording indicator
        // will check it before the success callback fires.
        this._setScreencastInProgress(true);
        let retry = false;

        try {
            this._screencastStarting = true;

            const [, path] = await method(
                GLib.build_filenamev([
                    /* Translators: this is the folder where recorded
                       screencasts are stored. */
                    _('Screencasts'),
                    /* Translators: this is a filename used for screencast
                     * recording, where "%d" and "%t" date and time, e.g.
                     * "Screencast from 07-17-2013 10:00:46 PM" */
                    /* xgettext:no-c-format */
                    _('Screencast from %d %t'),
                ]),
                {'draw-cursor': new GLib.Variant('b', drawCursor)});

            this._screencastPath = path;
        } catch (error) {
            // Recorder service disconnected without reply -> service crash
            // That should have blocklisted the pipeline that caused the crash,
            // so try again.
            if (error.matches(Gio.DBusError, Gio.DBusError.NO_REPLY) && nRetries < 2)
                retry = true;
            else
                this._screencastFailed(ScreencastPhase.STARTUP, error);
        }

        delete this._screencastStarting;

        if (retry) {
            console.log('Screencast service crashed during startup, trying again');
            this._setScreencastInProgress(false);
            this._startScreencast(nRetries + 1);
        }
    }

    async stopScreencast() {
        if (!this._screencastInProgress)
            return;

        // Set this before calling the method as the screen recording indicator
        // will check it before the success callback fires.
        this._setScreencastInProgress(false);

        try {
            const [success] = await this._screencastProxy.StopScreencastAsync();
            if (!success)
                throw new Error();
        } catch (error) {
            const {message} = error;
            if (message)
                log(`Error stopping screencast: ${message}`);
            else
                log('Error stopping screencast');
            return;
        }

        // Translators: notification title.
        this._showNotification(_('Screencast recorded'));
    }

    _screencastFailed(phase, error) {
        console.error(`Screencast failed during phase ${phase}: ${error}`);

        this._setScreencastInProgress(false);

        switch (phase) {
        case ScreencastPhase.STARTUP:
            delete this._screencastPath;

            // Translators: notification title.
            this._showNotification(_('Screencast failed to start'));
            break;

        case ScreencastPhase.RECORDING:
            if (error.matches(ScreencastErrors, ScreencastError.OUT_OF_DISK_SPACE)) {
                // Translators: notification title.
                this._showNotification(_('Screencast ended: Out of disk space'));
            } else if (error.matches(ScreencastErrors, ScreencastError.SERVICE_CRASH)) {
                // We can encourage user to try again on service crashes since the
                // recorder will auto-blocklist the pipeline that crashed.

                // Translators: notification title.
                this._showNotification(_('Screencast ended unexpectedly, please try again'));
            } else {
                // Translators: notification title.
                this._showNotification(_('Screencast ended unexpectedly'));
            }

            break;
        }
    }

    _showNotification(title) {
        const source = new MessageTray.Source({
            // Translators: notification source name.
            title: _('Screenshot'),
            iconName: 'screencast-recorded-symbolic',
        });
        const notification = new MessageTray.Notification({
            source,
            title,
            // Translators: notification body when a screencast was recorded.
            body: this._screencastPath ? _('Click here to view the video.') : '',
            isTransient: true,
        });

        if (this._screencastPath) {
            const file = Gio.file_new_for_path(this._screencastPath);

            // Translators: button on the screencast notification.
            notification.addAction(_('Show in Files'), () => {
                const app =
                    Gio.app_info_get_default_for_type('inode/directory', false);

                if (app === null) {
                    // It may be null e.g. in a toolbox without nautilus.
                    log('Error showing in files: no default app set for inode/directory');
                    return;
                }

                app.launch([file], global.create_app_launch_context(0, -1));
            });
            notification.connect('activated', () => {
                try {
                    Gio.app_info_launch_default_for_uri(
                        file.get_uri(), global.create_app_launch_context(0, -1));
                } catch (err) {
                    logError(err, 'Error opening screencast');
                }
            });
        }

        Main.messageTray.add(source);
        source.addNotification(notification);
    }

    get screencast_in_progress() {
        return this._screencastInProgress;
    }

    _setScreencastInProgress(inProgress) {
        if (this._screencastInProgress === inProgress)
            return;

        this._screencastInProgress = inProgress;
        this.notify('screencast-in-progress');
    }

    vfunc_key_press_event(event) {
        const symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_Return || symbol === Clutter.KEY_space ||
            symbol === Clutter.KEY_KP_Enter || symbol === Clutter.KEY_ISO_Enter ||
            ((event.get_state() & Clutter.ModifierType.CONTROL_MASK) &&
             (symbol === Clutter.KEY_c || symbol === Clutter.KEY_C))) {
            this._onCaptureButtonClicked();
            return Clutter.EVENT_STOP;
        }

        if (symbol === Clutter.KEY_s || symbol === Clutter.KEY_S) {
            this._selectionButton.checked = true;
            return Clutter.EVENT_STOP;
        }

        if (symbol === Clutter.KEY_c || symbol === Clutter.KEY_C) {
            this._screenButton.checked = true;
            return Clutter.EVENT_STOP;
        }

        if (this._windowButton.reactive &&
            (symbol === Clutter.KEY_w || symbol === Clutter.KEY_W)) {
            this._windowButton.checked = true;
            return Clutter.EVENT_STOP;
        }

        if (symbol === Clutter.KEY_p || symbol === Clutter.KEY_P) {
            this._showPointerButton.checked = !this._showPointerButton.checked;
            return Clutter.EVENT_STOP;
        }

        if (this._castButton.reactive &&
            (symbol === Clutter.KEY_v || symbol === Clutter.KEY_V)) {
            this._castButton.checked = !this._castButton.checked;
            return Clutter.EVENT_STOP;
        }

        if (symbol === Clutter.KEY_Left || symbol === Clutter.KEY_Right ||
            symbol === Clutter.KEY_Up || symbol === Clutter.KEY_Down) {
            let direction;
            if (symbol === Clutter.KEY_Left)
                direction = St.DirectionType.LEFT;
            else if (symbol === Clutter.KEY_Right)
                direction = St.DirectionType.RIGHT;
            else if (symbol === Clutter.KEY_Up)
                direction = St.DirectionType.UP;
            else if (symbol === Clutter.KEY_Down)
                direction = St.DirectionType.DOWN;

            if (this._windowButton.checked) {
                const window =
                    this._windowSelectors.flatMap(selector => selector.windows())
                        .find(win => win.checked) ?? null;
                this.navigate_focus(window, direction, false);
            } else if (this._screenButton.checked) {
                const screen =
                    this._screenSelectors.find(selector => selector.checked) ?? null;
                this.navigate_focus(screen, direction, false);
            }

            return Clutter.EVENT_STOP;
        }

        return super.vfunc_key_press_event(event);
    }
});

/**
 * Stores a PNG-encoded screenshot into the clipboard and a file, and shows a
 * notification.
 *
 * @param {GLib.Bytes} bytes - The PNG-encoded screenshot.
 * @param {GdkPixbuf.Pixbuf} pixbuf - The Pixbuf with the screenshot.
 */
function _storeScreenshot(bytes, pixbuf) {
    // Store to the clipboard first in case storing to file fails.
    const clipboard = St.Clipboard.get_default();
    clipboard.set_content(St.ClipboardType.CLIPBOARD, 'image/png', bytes);

    const time = GLib.DateTime.new_now_local();

    // This will be set in the first save to disk branch and then accessed
    // in the second save to disk branch, so we need to declare it outside.
    let file;

    // The function is declared here rather than inside the condition to
    // satisfy eslint.

    /**
     * Returns a filename suffix with an increasingly large index.
     *
     * @returns {Generator<string|*, void, *>} suffix string
     */
    function* suffixes() {
        yield '';

        for (let i = 1; ; i++)
            yield `-${i}`;
    }

    /**
     * Adds a record of a screenshot file in the recently used files list.
     *
     * @param {Gio.File} screenshotFile - The screenshot file.
     */
    function saveRecentFile(screenshotFile) {
        const recentFile =
            GLib.build_filenamev([GLib.get_user_data_dir(), 'recently-used.xbel']);
        const uri = screenshotFile.get_uri();
        const bookmarks = new GLib.BookmarkFile();
        try {
            bookmarks.load_from_file(recentFile);
        } catch (e) {
            if (!e.matches(GLib.BookmarkFileError, GLib.BookmarkFileError.FILE_NOT_FOUND)) {
                log(`Could not open recent file ${uri}: ${e.message}`);
                return;
            }
        }

        try {
            bookmarks.add_application(uri, GLib.get_prgname(), 'gio open %u');
            bookmarks.to_file(recentFile);
        } catch (e) {
            log(`Could not save recent file ${uri}: ${e.message}`);
        }
    }

    const lockdownSettings =
        new Gio.Settings({schema_id: 'org.gnome.desktop.lockdown'});
    const disableSaveToDisk =
        lockdownSettings.get_boolean('disable-save-to-disk');

    if (!disableSaveToDisk) {
        const dir = Gio.File.new_for_path(GLib.build_filenamev([
            GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES) || GLib.get_home_dir(),
            // Translators: name of the folder under ~/Pictures for screenshots.
            _('Screenshots'),
        ]));

        try {
            dir.make_directory_with_parents(null);
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
                throw e;
        }

        const timestamp = time.format('%Y-%m-%d %H-%M-%S');
        // Translators: this is the name of the file that the screenshot is
        // saved to. The placeholder is a timestamp, e.g. "2017-05-21 12-24-03".
        const name = _('Screenshot from %s').format(timestamp);

        // If the target file already exists, try appending a suffix with an
        // increasing number to it.
        for (const suffix of suffixes()) {
            file = Gio.File.new_for_path(GLib.build_filenamev([
                dir.get_path(), `${name}${suffix}.png`,
            ]));

            try {
                const stream = file.create(Gio.FileCreateFlags.NONE, null);
                stream.write_bytes(bytes, null);
                break;
            } catch (e) {
                if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
                    throw e;
            }
        }

        // Add it to recent files.
        saveRecentFile(file);
    }

    // Create a St.ImageContent icon for the notification. We want
    // St.ImageContent specifically because it preserves the aspect ratio when
    // shown in a notification.
    const pixels = pixbuf.read_pixel_bytes();
    const content =
        St.ImageContent.new_with_preferred_size(pixbuf.width, pixbuf.height);
    content.set_bytes(
        pixels,
        Cogl.PixelFormat.RGBA_8888,
        pixbuf.width,
        pixbuf.height,
        pixbuf.rowstride
    );

    // Show a notification.
    const source = new MessageTray.Source({
        // Translators: notification source name.
        title: _('Screenshot'),
        iconName: 'screenshot-recorded-symbolic',
    });
    const notification = new MessageTray.Notification({
        source,
        // Translators: notification title.
        title: _('Screenshot captured'),
        // Translators: notification body when a screenshot was captured.
        body: _('You can paste the image from the clipboard.'),
        datetime: time,
        gicon: content,
        isTransient: true,
    });

    if (!disableSaveToDisk) {
        // Translators: button on the screenshot notification.
        notification.addAction(_('Show in Files'), () => {
            const app =
                Gio.app_info_get_default_for_type('inode/directory', false);

            if (app === null) {
                // It may be null e.g. in a toolbox without nautilus.
                log('Error showing in files: no default app set for inode/directory');
                return;
            }

            app.launch([file], global.create_app_launch_context(0, -1));
        });
        notification.connect('activated', () => {
            try {
                Gio.app_info_launch_default_for_uri(
                    file.get_uri(), global.create_app_launch_context(0, -1));
            } catch (err) {
                logError(err, 'Error opening screenshot');
            }
        });
    }

    Main.messageTray.add(source);
    source.addNotification(notification);

    return file;
}

/**
 * Captures a screenshot from a texture, given a region, scale and optional
 * cursor data.
 *
 * @param {Cogl.Texture} texture - The texture to take the screenshot from.
 * @param {number[4]} [geometry] - The region to use: x, y, width and height.
 * @param {number} scale - The texture scale.
 * @param {object} [cursor] - Cursor data to include in the screenshot.
 * @param {Cogl.Texture} cursor.texture - The cursor texture.
 * @param {number} cursor.x - The cursor x coordinate.
 * @param {number} cursor.y - The cursor y coordinate.
 * @param {number} cursor.scale - The cursor texture scale.
 */
export async function captureScreenshot(texture, geometry, scale, cursor) {
    const stream = Gio.MemoryOutputStream.new_resizable();
    const [x, y, w, h] = geometry ?? [0, 0, -1, -1];
    if (cursor === null)
        cursor = {texture: null, x: 0, y: 0, scale: 1};

    global.display.get_sound_player().play_from_theme(
        'screen-capture', _('Screenshot taken'), null);

    const pixbuf = await Shell.Screenshot.composite_to_stream(
        texture,
        x, y, w, h,
        scale,
        cursor.texture, cursor.x, cursor.y, cursor.scale,
        stream
    );

    stream.close(null);
    return _storeScreenshot(stream.steal_as_bytes(), pixbuf);
}

/**
 * Shows the screenshot UI.
 */
export function showScreenshotUI() {
    Main.screenshotUI.open().catch(err => {
        logError(err, 'Error opening the screenshot UI');
    });
}

/**
 * Shows the screen recording UI.
 */
export function showScreenRecordingUI() {
    Main.screenshotUI.open(UIMode.SCREENCAST).catch(err => {
        logError(err, 'Error opening the screenshot UI');
    });
}

export class ScreenshotService {
    constructor() {
        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(ScreenshotIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Screenshot');

        this._screenShooter = new Map();
        this._senderChecker = new DBusSenderChecker([
            'org.gnome.SettingsDaemon.MediaKeys',
            'org.freedesktop.impl.portal.desktop.gtk',
            'org.freedesktop.impl.portal.desktop.gnome',
            'org.gnome.Screenshot',
        ]);

        this._lockdownSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.lockdown'});

        Gio.DBus.session.own_name('org.gnome.Shell.Screenshot', Gio.BusNameOwnerFlags.REPLACE, null, null);
    }

    async _createScreenshot(invocation, needsDisk = true, restrictCallers = true) {
        let lockedDown = false;
        if (needsDisk)
            lockedDown = this._lockdownSettings.get_boolean('disable-save-to-disk');

        let sender = invocation.get_sender();
        if (this._screenShooter.has(sender)) {
            invocation.return_error_literal(
                Gio.IOErrorEnum, Gio.IOErrorEnum.BUSY,
                'There is an ongoing operation for this sender');
            return null;
        } else if (lockedDown) {
            invocation.return_error_literal(
                Gio.IOErrorEnum, Gio.IOErrorEnum.PERMISSION_DENIED,
                'Saving to disk is disabled');
            return null;
        } else if (restrictCallers) {
            try {
                await this._senderChecker.checkInvocation(invocation);
            } catch (e) {
                invocation.return_gerror(e);
                return null;
            }
        }

        let shooter = new Shell.Screenshot();
        shooter._watchNameId = Gio.bus_watch_name(Gio.BusType.SESSION,
            sender, 0, null, this._onNameVanished.bind(this));

        this._screenShooter.set(sender, shooter);

        return shooter;
    }

    _onNameVanished(connection, name) {
        this._removeShooterForSender(name);
    }

    _removeShooterForSender(sender) {
        let shooter = this._screenShooter.get(sender);
        if (!shooter)
            return;

        Gio.bus_unwatch_name(shooter._watchNameId);
        this._screenShooter.delete(sender);
    }

    _checkArea(x, y, width, height) {
        return x >= 0 && y >= 0 &&
               width > 0 && height > 0 &&
               x + width <= global.screen_width &&
               y + height <= global.screen_height;
    }

    *_resolveRelativeFilename(filename) {
        filename = filename.replace(/\.png$/, '');

        let path = [
            GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES),
            GLib.get_home_dir(),
        ].find(p => p && GLib.file_test(p, GLib.FileTest.EXISTS));

        if (!path)
            return null;

        yield Gio.File.new_for_path(
            GLib.build_filenamev([path, `${filename}.png`]));

        for (let idx = 1; ; idx++) {
            yield Gio.File.new_for_path(
                GLib.build_filenamev([path, `${filename}-${idx}.png`]));
        }
    }

    _createStream(filename, invocation) {
        if (filename === '')
            return [Gio.MemoryOutputStream.new_resizable(), null];

        if (GLib.path_is_absolute(filename)) {
            try {
                let file = Gio.File.new_for_path(filename);
                let stream = file.replace(null, false, Gio.FileCreateFlags.NONE, null);
                return [stream, file];
            } catch (e) {
                invocation.return_gerror(e);
                this._removeShooterForSender(invocation.get_sender());
                return [null, null];
            }
        }

        let err;
        for (let file of this._resolveRelativeFilename(filename)) {
            try {
                let stream = file.create(Gio.FileCreateFlags.NONE, null);
                return [stream, file];
            } catch (e) {
                err = e;
                if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
                    break;
            }
        }

        invocation.return_gerror(err);
        this._removeShooterForSender(invocation.get_sender());
        return [null, null];
    }

    _flashAsync(shooter) {
        return new Promise((resolve, _reject) => {
            shooter.connect('screenshot_taken', (s, area) => {
                const flashspot = new Flashspot(area);
                flashspot.fire(resolve);

                global.display.get_sound_player().play_from_theme(
                    'screen-capture', _('Screenshot taken'), null);
            });
        });
    }

    _onScreenshotComplete(stream, file, invocation) {
        stream.close(null);

        let filenameUsed = '';
        if (file) {
            filenameUsed = file.get_path();
        } else {
            let bytes = stream.steal_as_bytes();
            let clipboard = St.Clipboard.get_default();
            clipboard.set_content(St.ClipboardType.CLIPBOARD, 'image/png', bytes);
        }

        let retval = GLib.Variant.new('(bs)', [true, filenameUsed]);
        invocation.return_value(retval);
    }

    _scaleArea(x, y, width, height) {
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        x *= scaleFactor;
        y *= scaleFactor;
        width *= scaleFactor;
        height *= scaleFactor;
        return [x, y, width, height];
    }

    _unscaleArea(x, y, width, height) {
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        x /= scaleFactor;
        y /= scaleFactor;
        width /= scaleFactor;
        height /= scaleFactor;
        return [x, y, width, height];
    }

    async ScreenshotAreaAsync(params, invocation) {
        let [x, y, width, height, flash, filename] = params;
        [x, y, width, height] = this._scaleArea(x, y, width, height);
        if (!this._checkArea(x, y, width, height)) {
            invocation.return_error_literal(
                Gio.IOErrorEnum,
                Gio.IOErrorEnum.CANCELLED,
                'Invalid params');
            return;
        }
        let screenshot = await this._createScreenshot(invocation);
        if (!screenshot)
            return;

        let [stream, file] = this._createStream(filename, invocation);
        if (!stream)
            return;

        try {
            await Promise.all([
                flash ? this._flashAsync(screenshot) : null,
                screenshot.screenshot_area(x, y, width, height, stream),
            ]);
            this._onScreenshotComplete(stream, file, invocation);
        } catch (e) {
            invocation.return_value(new GLib.Variant('(bs)', [false, '']));
        } finally {
            this._removeShooterForSender(invocation.get_sender());
        }
    }

    async ScreenshotWindowAsync(params, invocation) {
        let [includeFrame, includeCursor, flash, filename] = params;
        let screenshot = await this._createScreenshot(invocation);
        if (!screenshot)
            return;

        let [stream, file] = this._createStream(filename, invocation);
        if (!stream)
            return;

        try {
            await Promise.all([
                flash ? this._flashAsync(screenshot) : null,
                screenshot.screenshot_window(includeFrame, includeCursor, stream),
            ]);
            this._onScreenshotComplete(stream, file, invocation);
        } catch (e) {
            invocation.return_value(new GLib.Variant('(bs)', [false, '']));
        } finally {
            this._removeShooterForSender(invocation.get_sender());
        }
    }

    async ScreenshotAsync(params, invocation) {
        let [includeCursor, flash, filename] = params;
        let screenshot = await this._createScreenshot(invocation);
        if (!screenshot)
            return;

        let [stream, file] = this._createStream(filename, invocation);
        if (!stream)
            return;

        try {
            await Promise.all([
                flash ? this._flashAsync(screenshot) : null,
                screenshot.screenshot(includeCursor, stream),
            ]);
            this._onScreenshotComplete(stream, file, invocation);
        } catch (e) {
            invocation.return_value(new GLib.Variant('(bs)', [false, '']));
        } finally {
            this._removeShooterForSender(invocation.get_sender());
        }
    }

    async InteractiveScreenshotAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        Main.screenshotUI.connectObject(
            'screenshot-taken', (ui, file) => {
                Main.screenshotUI.disconnectObject(invocation);
                invocation.return_value(new GLib.Variant('(bs)', [true, file.get_uri()]));
            },
            'closed', () => {
                Main.screenshotUI.disconnectObject(invocation);
                invocation.return_value(new GLib.Variant('(bs)', [false, '']));
            },
            invocation);


        try {
            Main.screenshotUI.open(UIMode.SCREENSHOT_ONLY);
        } catch (e) {
            Main.screenshotUI.disconnectObject(invocation);
            invocation.return_value(new GLib.Variant('(bs)', [false, '']));
        }
    }

    async SelectAreaAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        let selectArea = new SelectArea();
        try {
            let areaRectangle = await selectArea.selectAsync();
            let retRectangle = this._unscaleArea(
                areaRectangle.x, areaRectangle.y,
                areaRectangle.width, areaRectangle.height);
            invocation.return_value(GLib.Variant.new('(iiii)', retRectangle));
        } catch (e) {
            invocation.return_error_literal(
                Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED,
                'Operation was cancelled');
        }
    }

    async FlashAreaAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        let [x, y, width, height] = params;
        [x, y, width, height] = this._scaleArea(x, y, width, height);
        if (!this._checkArea(x, y, width, height)) {
            invocation.return_error_literal(
                Gio.IOErrorEnum,
                Gio.IOErrorEnum.CANCELLED,
                'Invalid params');
            return;
        }
        let flashspot = new Flashspot({x, y, width, height});
        flashspot.fire();
        invocation.return_value(null);
    }

    async PickColorAsync(params, invocation) {
        const screenshot = await this._createScreenshot(invocation, false, false);
        if (!screenshot)
            return;

        const pickPixel = new PickPixel(screenshot);
        try {
            const color = await pickPixel.pickAsync();
            const {red, green, blue} = color;
            const retval = GLib.Variant.new('(a{sv})', [{
                color: GLib.Variant.new('(ddd)', [
                    red / 255.0,
                    green / 255.0,
                    blue / 255.0,
                ]),
            }]);
            invocation.return_value(retval);
        } catch (e) {
            invocation.return_error_literal(
                Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED,
                'Operation was cancelled');
        } finally {
            this._removeShooterForSender(invocation.get_sender());
        }
    }
}

export const SelectArea = GObject.registerClass(
class SelectArea extends St.Widget {
    _init() {
        this._startX = -1;
        this._startY = -1;
        this._lastX = 0;
        this._lastY = 0;
        this._result = null;

        super._init({
            visible: false,
            reactive: true,
            x: 0,
            y: 0,
        });
        Main.uiGroup.add_child(this);

        this._grabHelper = new GrabHelper.GrabHelper(this);

        const constraint = new Clutter.BindConstraint({
            source: global.stage,
            coordinate: Clutter.BindCoordinate.ALL,
        });
        this.add_constraint(constraint);

        this._rubberband = new St.Widget({
            style_class: 'select-area-rubberband',
            visible: false,
        });
        this.add_child(this._rubberband);
    }

    async selectAsync() {
        global.display.set_cursor(Meta.Cursor.CROSSHAIR);
        Main.uiGroup.set_child_above_sibling(this, null);
        this.show();

        try {
            await this._grabHelper.grabAsync({actor: this});
        } finally {
            global.display.set_cursor(Meta.Cursor.DEFAULT);

            GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                this.destroy();
                return GLib.SOURCE_REMOVE;
            });
        }

        return this._result;
    }

    _getGeometry() {
        return new Mtk.Rectangle({
            x: Math.min(this._startX, this._lastX),
            y: Math.min(this._startY, this._lastY),
            width: Math.abs(this._startX - this._lastX) + 1,
            height: Math.abs(this._startY - this._lastY) + 1,
        });
    }

    vfunc_motion_event(event) {
        if (this._startX === -1 || this._startY === -1 || this._result)
            return Clutter.EVENT_PROPAGATE;

        [this._lastX, this._lastY] = event.get_coords();
        this._lastX = Math.floor(this._lastX);
        this._lastY = Math.floor(this._lastY);
        let geometry = this._getGeometry();

        this._rubberband.set_position(geometry.x, geometry.y);
        this._rubberband.set_size(geometry.width, geometry.height);
        this._rubberband.show();

        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_button_press_event(event) {
        if (this._result)
            return Clutter.EVENT_PROPAGATE;

        [this._startX, this._startY] = event.get_coords();
        this._startX = Math.floor(this._startX);
        this._startY = Math.floor(this._startY);
        this._rubberband.set_position(this._startX, this._startY);

        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_button_release_event() {
        if (this._startX === -1 || this._startY === -1 || this._result)
            return Clutter.EVENT_PROPAGATE;

        this._result = this._getGeometry();
        this.ease({
            opacity: 0,
            duration: 200,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this._grabHelper.ungrab(),
        });
        return Clutter.EVENT_PROPAGATE;
    }
});

const RecolorEffect = GObject.registerClass({
    Properties: {
        color: GObject.ParamSpec.boxed(
            'color', 'color', 'replacement color',
            GObject.ParamFlags.WRITABLE,
            Clutter.Color.$gtype),
        chroma: GObject.ParamSpec.boxed(
            'chroma', 'chroma', 'color to replace',
            GObject.ParamFlags.WRITABLE,
            Clutter.Color.$gtype),
        threshold: GObject.ParamSpec.float(
            'threshold', 'threshold', 'threshold',
            GObject.ParamFlags.WRITABLE,
            0.0, 1.0, 0.0),
        smoothing: GObject.ParamSpec.float(
            'smoothing', 'smoothing', 'smoothing',
            GObject.ParamFlags.WRITABLE,
            0.0, 1.0, 0.0),
    },
}, class RecolorEffect extends Shell.GLSLEffect {
    _init(params) {
        this._color = new Clutter.Color();
        this._chroma = new Clutter.Color();
        this._threshold = 0;
        this._smoothing = 0;

        this._colorLocation = null;
        this._chromaLocation = null;
        this._thresholdLocation = null;
        this._smoothingLocation = null;

        super._init(params);

        this._colorLocation = this.get_uniform_location('recolor_color');
        this._chromaLocation = this.get_uniform_location('chroma_color');
        this._thresholdLocation = this.get_uniform_location('threshold');
        this._smoothingLocation = this.get_uniform_location('smoothing');

        this._updateColorUniform(this._colorLocation, this._color);
        this._updateColorUniform(this._chromaLocation, this._chroma);
        this._updateFloatUniform(this._thresholdLocation, this._threshold);
        this._updateFloatUniform(this._smoothingLocation, this._smoothing);
    }

    _updateColorUniform(location, color) {
        if (!location)
            return;

        this.set_uniform_float(location,
            3, [color.red / 255, color.green / 255, color.blue / 255]);
        this.queue_repaint();
    }

    _updateFloatUniform(location, value) {
        if (!location)
            return;

        this.set_uniform_float(location, 1, [value]);
        this.queue_repaint();
    }

    set color(c) {
        if (this._color.equal(c))
            return;

        this._color = c;
        this.notify('color');

        this._updateColorUniform(this._colorLocation, this._color);
    }

    set chroma(c) {
        if (this._chroma.equal(c))
            return;

        this._chroma = c;
        this.notify('chroma');

        this._updateColorUniform(this._chromaLocation, this._chroma);
    }

    set threshold(value) {
        if (this._threshold === value)
            return;

        this._threshold = value;
        this.notify('threshold');

        this._updateFloatUniform(this._thresholdLocation, this._threshold);
    }

    set smoothing(value) {
        if (this._smoothing === value)
            return;

        this._smoothing = value;
        this.notify('smoothing');

        this._updateFloatUniform(this._smoothingLocation, this._smoothing);
    }

    vfunc_build_pipeline() {
        // Conversion parameters from https://en.wikipedia.org/wiki/YCbCr
        const decl = `
            vec3 rgb2yCrCb(vec3 c) {                                \n
                float y = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;  \n
                float cr = 0.7133 * (c.r - y);                      \n
                float cb = 0.5643 * (c.b - y);                      \n
                return vec3(y, cr, cb);                             \n
            }                                                       \n
                                                                    \n
            uniform vec3 chroma_color;                              \n
            uniform vec3 recolor_color;                             \n
            uniform float threshold;                                \n
            uniform float smoothing;                                \n`;
        const src = `
            vec3 mask = rgb2yCrCb(chroma_color.rgb);                \n
            vec3 yCrCb = rgb2yCrCb(cogl_color_out.rgb);             \n
            float blend =                                           \n
              smoothstep(threshold,                                 \n
                         threshold + smoothing,                     \n
                         distance(yCrCb.gb, mask.gb));              \n
            cogl_color_out.rgb =                                    \n
              mix(recolor_color, cogl_color_out.rgb, blend);        \n`;

        this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, decl, src, false);
    }
});

export const PickPixel = GObject.registerClass(
class PickPixel extends St.Widget {
    _init(screenshot) {
        super._init({visible: false, reactive: true});

        this._screenshot = screenshot;

        this._result = null;
        this._color = null;
        this._inPick = false;

        Main.uiGroup.add_child(this);

        this._grabHelper = new GrabHelper.GrabHelper(this);

        const constraint = new Clutter.BindConstraint({
            source: global.stage,
            coordinate: Clutter.BindCoordinate.ALL,
        });
        this.add_constraint(constraint);

        const action = new Clutter.ClickAction();
        action.connect('clicked', async () => {
            await this._pickColor(...action.get_coords());
            this._result = this._color;
            this._grabHelper.ungrab();
        });
        this.add_action(action);

        this._recolorEffect = new RecolorEffect({
            chroma: new Clutter.Color({
                red: 80,
                green: 219,
                blue: 181,
            }),
            threshold: 0.04,
            smoothing: 0.07,
        });
        this._previewCursor = new St.Icon({
            icon_name: 'color-pick',
            icon_size: Meta.prefs_get_cursor_size(),
            effect: this._recolorEffect,
            visible: false,
        });
        Main.uiGroup.add_child(this._previewCursor);
    }

    async pickAsync() {
        global.display.set_cursor(Meta.Cursor.BLANK);
        Main.uiGroup.set_child_above_sibling(this, null);
        this.show();

        this._pickColor(...global.get_pointer());

        try {
            await this._grabHelper.grabAsync({actor: this});
        } finally {
            global.display.set_cursor(Meta.Cursor.DEFAULT);
            this._previewCursor.destroy();

            GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                this.destroy();
                return GLib.SOURCE_REMOVE;
            });
        }

        return this._result;
    }

    async _pickColor(x, y) {
        if (this._inPick)
            return;

        this._inPick = true;
        this._previewCursor.set_position(x, y);
        [this._color] = await this._screenshot.pick_color(x, y);
        this._inPick = false;

        if (!this._color)
            return;

        this._recolorEffect.color = this._color;
        this._previewCursor.show();
    }

    vfunc_motion_event(event) {
        const [x, y] = event.get_coords();
        this._pickColor(x, y);
        return Clutter.EVENT_PROPAGATE;
    }
});

const FLASHSPOT_ANIMATION_OUT_TIME = 500; // milliseconds

export const Flashspot = GObject.registerClass(
class Flashspot extends Lightbox.Lightbox {
    _init(area) {
        super._init(Main.uiGroup, {
            inhibitEvents: true,
            width: area.width,
            height: area.height,
        });
        this.style_class = 'flashspot';
        this.set_position(area.x, area.y);
    }

    fire(doneCallback) {
        this.set({visible: true, opacity: 255});
        this.ease({
            opacity: 0,
            duration: FLASHSPOT_ANIMATION_OUT_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                if (doneCallback)
                    doneCallback();
                this.destroy();
            },
        });
    }
});
(uuay)endSessionDialog.jsm// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/*
 * Copyright 2010-2016 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

import AccountsService from 'gi://AccountsService';
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import Polkit from 'gi://Polkit';
import Shell from 'gi://Shell';
import St from 'gi://St';
import UPower from 'gi://UPowerGlib';

import * as CheckBox from './checkBox.js';
import * as Dialog from './dialog.js';
import * as GnomeSession from '../misc/gnomeSession.js';
import * as LoginManager from '../misc/loginManager.js';
import * as ModalDialog from './modalDialog.js';
import * as UserWidget from './userWidget.js';

import {ModalDialogErrors, ModalDialogError} from '../misc/dbusErrors.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';

const _ITEM_ICON_SIZE = 64;

const LOW_BATTERY_THRESHOLD = 30;

const EndSessionDialogIface = loadInterfaceXML('org.gnome.SessionManager.EndSessionDialog');

const logoutDialogContent = {
    subjectWithUser: C_('title', 'Log Out %s'),
    subject: C_('title', 'Log Out'),
    descriptionWithUser(user, seconds) {
        return ngettext(
            '%s will be logged out automatically in %d second.',
            '%s will be logged out automatically in %d seconds.',
            seconds).format(user, seconds);
    },
    description(seconds) {
        return ngettext(
            'You will be logged out automatically in %d second.',
            'You will be logged out automatically in %d seconds.',
            seconds).format(seconds);
    },
    showBatteryWarning: false,
    confirmButtons: [{
        signal: 'ConfirmedLogout',
        label: C_('button', 'Log Out'),
    }],
    showOtherSessions: false,
};

const shutdownDialogContent = {
    subject: C_('title', 'Power Off'),
    subjectWithUpdates: C_('title', 'Install Updates & Power Off'),
    description(seconds) {
        return ngettext(
            'The system will power off automatically in %d second.',
            'The system will power off automatically in %d seconds.',
            seconds).format(seconds);
    },
    checkBoxText: C_('checkbox', 'Install pending software updates'),
    showBatteryWarning: true,
    confirmButtons: [{
        signal: 'ConfirmedShutdown',
        label: C_('button', 'Power Off'),
    }],
    iconName: 'system-shutdown-symbolic',
    showOtherSessions: true,
};

const restartDialogContent = {
    subject: C_('title', 'Restart'),
    subjectWithUpdates: C_('title', 'Install Updates & Restart'),
    description(seconds) {
        return ngettext(
            'The system will restart automatically in %d second.',
            'The system will restart automatically in %d seconds.',
            seconds).format(seconds);
    },
    checkBoxText: C_('checkbox', 'Install pending software updates'),
    showBatteryWarning: true,
    confirmButtons: [{
        signal: 'ConfirmedReboot',
        label: C_('button', 'Restart'),
    }],
    iconName: 'view-refresh-symbolic',
    showOtherSessions: true,
};

const restartUpdateDialogContent = {

    subject: C_('title', 'Restart & Install Updates'),
    description(seconds) {
        return ngettext(
            'The system will automatically restart and install updates in %d second.',
            'The system will automatically restart and install updates in %d seconds.',
            seconds).format(seconds);
    },
    showBatteryWarning: true,
    confirmButtons: [{
        signal: 'ConfirmedReboot',
        label: C_('button', 'Restart & Install'),
    }],
    unusedFutureButtonForTranslation: C_('button', 'Install & Power Off'),
    unusedFutureCheckBoxForTranslation: C_('checkbox', 'Power off after updates are installed'),
    iconName: 'view-refresh-symbolic',
    showOtherSessions: true,
};

const restartUpgradeDialogContent = {

    subject: C_('title', 'Restart & Install Upgrade'),
    upgradeDescription(distroName, distroVersion) {
        /* Translators: This is the text displayed for system upgrades in the
           shut down dialog. First %s gets replaced with the distro name and
           second %s with the distro version to upgrade to */
        return _('%s %s will be installed after restart. Upgrade installation can take a long time: ensure that you have backed up and that the computer is plugged in.').format(distroName, distroVersion);
    },
    disableTimer: true,
    showBatteryWarning: false,
    confirmButtons: [{
        signal: 'ConfirmedReboot',
        label: C_('button', 'Restart & Install'),
    }],
    iconName: 'view-refresh-symbolic',
    showOtherSessions: true,
};

const DialogType = {
    LOGOUT: 0 /* GSM_SHELL_END_SESSION_DIALOG_TYPE_LOGOUT */,
    SHUTDOWN: 1 /* GSM_SHELL_END_SESSION_DIALOG_TYPE_SHUTDOWN */,
    RESTART: 2 /* GSM_SHELL_END_SESSION_DIALOG_TYPE_RESTART */,
    UPDATE_RESTART: 3,
    UPGRADE_RESTART: 4,
};

const DialogContent = {
    0 /* DialogType.LOGOUT */: logoutDialogContent,
    1 /* DialogType.SHUTDOWN */: shutdownDialogContent,
    2 /* DialogType.RESTART */: restartDialogContent,
    3 /* DialogType.UPDATE_RESTART */: restartUpdateDialogContent,
    4 /* DialogType.UPGRADE_RESTART */: restartUpgradeDialogContent,
};

const MAX_USERS_IN_SESSION_DIALOG = 5;

const LogindSessionIface = loadInterfaceXML('org.freedesktop.login1.Session');
const LogindSession = Gio.DBusProxy.makeProxyWrapper(LogindSessionIface);

const PkOfflineIface = loadInterfaceXML('org.freedesktop.PackageKit.Offline');
const PkOfflineProxy = Gio.DBusProxy.makeProxyWrapper(PkOfflineIface);

const UPowerIface = loadInterfaceXML('org.freedesktop.UPower.Device');
const UPowerProxy = Gio.DBusProxy.makeProxyWrapper(UPowerIface);

function findAppFromInhibitor(inhibitor) {
    let desktopFile;
    try {
        [desktopFile] = inhibitor.GetAppIdSync();
    } catch (e) {
        // XXX -- sometimes JIT inhibitors generated by gnome-session
        // get removed too soon. Don't fail in this case.
        log(`gnome-session gave us a dead inhibitor: ${inhibitor.get_object_path()}`);
        return null;
    }

    if (!GLib.str_has_suffix(desktopFile, '.desktop'))
        desktopFile += '.desktop';

    return Shell.AppSystem.get_default().lookup_heuristic_basename(desktopFile);
}

// The logout timer only shows updates every 10 seconds
// until the last 10 seconds, then it shows updates every
// second.  This function takes a given time and returns
// what we should show to the user for that time.
function _roundSecondsToInterval(totalSeconds, secondsLeft, interval) {
    let time;

    time = Math.ceil(secondsLeft);

    // Final count down is in decrements of 1
    if (time <= interval)
        return time;

    // Round up higher than last displayable time interval
    time += interval - 1;

    // Then round down to that time interval
    if (time > totalSeconds)
        time = Math.ceil(totalSeconds);
    else
        time -= time % interval;

    return time;
}

function _setCheckBoxLabel(checkBox, text) {
    let label = checkBox.getLabelActor();

    if (text) {
        label.set_text(text);
        checkBox.show();
    } else {
        label.set_text('');
        checkBox.hide();
    }
}

export const EndSessionDialog = GObject.registerClass(
class EndSessionDialog extends ModalDialog.ModalDialog {
    _init() {
        super._init({
            styleClass: 'end-session-dialog',
            destroyOnClose: false,
        });

        this._loginManager = LoginManager.getLoginManager();
        this._canRebootToBootLoaderMenu = false;
        this._getCanRebootToBootLoaderMenu().catch(logError);

        this._userManager = AccountsService.UserManager.get_default();
        this._user = this._userManager.get_user(GLib.get_user_name());
        this._updatesPermission = null;

        this._pkOfflineProxy = new PkOfflineProxy(Gio.DBus.system,
            'org.freedesktop.PackageKit',
            '/org/freedesktop/PackageKit',
            this._onPkOfflineProxyCreated.bind(this));

        this._powerProxy = new UPowerProxy(Gio.DBus.system,
            'org.freedesktop.UPower',
            '/org/freedesktop/UPower/devices/DisplayDevice',
            (proxy, error) => {
                if (error) {
                    log(error.message);
                    return;
                }
                this._powerProxy.connect('g-properties-changed',
                    this._sync.bind(this));
                this._sync();
            });

        this._secondsLeft = 0;
        this._totalSecondsToStayOpen = 0;
        this._applications = [];
        this._sessions = [];
        this._capturedEventId = 0;
        this._rebootButton = null;
        this._rebootButtonAlt = null;

        this.connect('opened', this._onOpened.bind(this));

        this._user.connectObject(
            'notify::is-loaded', this._sync.bind(this),
            'changed', this._sync.bind(this), this);

        this._messageDialogContent = new Dialog.MessageDialogContent();

        this._checkBox = new CheckBox.CheckBox();
        this._checkBox.connect('clicked', this._sync.bind(this));
        this._messageDialogContent.add_child(this._checkBox);

        this._batteryWarning = new St.Label({
            style_class: 'end-session-dialog-battery-warning',
            text: _('Low battery power: please plug in before installing updates.'),
        });
        this._batteryWarning.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this._batteryWarning.clutter_text.line_wrap = true;
        this._messageDialogContent.add_child(this._batteryWarning);

        this.contentLayout.add_child(this._messageDialogContent);

        this._applicationSection = new Dialog.ListSection({
            title: _('Some applications are busy or have unsaved work'),
        });
        this.contentLayout.add_child(this._applicationSection);

        this._sessionSection = new Dialog.ListSection({
            title: _('Other users are logged in'),
        });
        this.contentLayout.add_child(this._sessionSection);

        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(EndSessionDialogIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gnome/SessionManager/EndSessionDialog');
    }

    async _getCanRebootToBootLoaderMenu() {
        const {canRebootToBootLoaderMenu} = await this._loginManager.canRebootToBootLoaderMenu();
        this._canRebootToBootLoaderMenu = canRebootToBootLoaderMenu;
    }

    async _onPkOfflineProxyCreated(proxy, error) {
        if (error) {
            log(error.message);
            return;
        }

        // Creating a D-Bus proxy won't propagate SERVICE_UNKNOWN or NAME_HAS_NO_OWNER
        // errors if PackageKit is not available, but the GIO implementation will make
        // sure in that case that the proxy's g-name-owner is set to null, so check that.
        if (this._pkOfflineProxy.g_name_owner === null) {
            this._pkOfflineProxy = null;
            return;
        }

        // It only makes sense to check for this permission if PackageKit is available.
        try {
            this._updatesPermission = await Polkit.Permission.new(
                'org.freedesktop.packagekit.trigger-offline-update', null, null);
        } catch (e) {
            log(`No permission to trigger offline updates: ${e}`);
        }
    }

    _isDischargingBattery() {
        return this._powerProxy.IsPresent &&
            this._powerProxy.State !== UPower.DeviceState.CHARGING &&
            this._powerProxy.State !== UPower.DeviceState.FULLY_CHARGED;
    }

    _isBatteryLow() {
        return this._isDischargingBattery() && this._powerProxy.Percentage < LOW_BATTERY_THRESHOLD;
    }

    _shouldShowLowBatteryWarning(dialogContent) {
        if (!dialogContent.showBatteryWarning)
            return false;

        if (!this._isBatteryLow())
            return false;

        if (this._checkBox.checked)
            return true;

        // Show the warning if updates have already been triggered, but
        // the user doesn't have enough permissions to cancel them.
        let updatesAllowed = this._updatesPermission && this._updatesPermission.allowed;
        return this._updateInfo.UpdatePrepared && this._updateInfo.UpdateTriggered && !updatesAllowed;
    }

    _sync() {
        let open = this.state === ModalDialog.State.OPENING || this.state === ModalDialog.State.OPENED;
        if (!open)
            return;

        let dialogContent = DialogContent[this._type];

        let subject = dialogContent.subject;

        // Use different title when we are installing updates
        if (dialogContent.subjectWithUpdates && this._checkBox.checked)
            subject = dialogContent.subjectWithUpdates;

        this._batteryWarning.visible = this._shouldShowLowBatteryWarning(dialogContent);

        let description;
        let displayTime = _roundSecondsToInterval(
            this._totalSecondsToStayOpen, this._secondsLeft, 10);

        if (this._user.is_loaded) {
            let realName = this._user.get_real_name();

            if (realName != null) {
                if (dialogContent.subjectWithUser)
                    subject = dialogContent.subjectWithUser.format(realName);

                if (dialogContent.descriptionWithUser)
                    description = dialogContent.descriptionWithUser(realName, displayTime);
            }
        }

        // Use a different description when we are installing a system upgrade
        // if the PackageKit proxy is available (i.e. PackageKit is available).
        if (dialogContent.upgradeDescription) {
            const {name, version} = this._updateInfo.PreparedUpgrade;
            if (name != null && version != null)
                description = dialogContent.upgradeDescription(name, version);
        }

        // Fall back to regular description
        if (!description)
            description = dialogContent.description(displayTime);

        this._messageDialogContent.title = subject;
        this._messageDialogContent.description = description;

        let hasApplications = this._applications.length > 0;
        let hasSessions = this._sessions.length > 0;

        this._applicationSection.visible = hasApplications;
        this._sessionSection.visible = hasSessions;
    }

    _onCapturedEvent(actor, event) {
        let altEnabled = false;

        let type = event.type();
        if (type !== Clutter.EventType.KEY_PRESS && type !== Clutter.EventType.KEY_RELEASE)
            return Clutter.EVENT_PROPAGATE;

        let key = event.get_key_symbol();
        if (key !== Clutter.KEY_Alt_L && key !== Clutter.KEY_Alt_R)
            return Clutter.EVENT_PROPAGATE;

        if (type === Clutter.EventType.KEY_PRESS)
            altEnabled = true;

        this._rebootButton.visible = !altEnabled;
        this._rebootButtonAlt.visible = altEnabled;

        return Clutter.EVENT_PROPAGATE;
    }

    _updateButtons() {
        this.clearButtons();

        this.addButton({
            action: this.cancel.bind(this),
            label: _('Cancel'),
            key: Clutter.KEY_Escape,
        });

        let dialogContent = DialogContent[this._type];
        for (let i = 0; i < dialogContent.confirmButtons.length; i++) {
            let signal = dialogContent.confirmButtons[i].signal;
            let label = dialogContent.confirmButtons[i].label;
            let button = this.addButton({
                action: () => {
                    let signalId = this.connect('closed', () => {
                        this.disconnect(signalId);
                        this._confirm(signal).catch(logError);
                    });
                    this.close(true);
                },
                label,
            });

            // Add Alt "Boot Options" option to the Reboot button
            if (this._canRebootToBootLoaderMenu && signal === 'ConfirmedReboot') {
                this._rebootButton = button;
                this._rebootButtonAlt = this.addButton({
                    action: () => {
                        this.close(true);
                        let signalId = this.connect('closed', () => {
                            this.disconnect(signalId);
                            this._confirmRebootToBootLoaderMenu();
                        });
                    },
                    label: C_('button', 'Boot Options'),
                });
                this._rebootButtonAlt.visible = false;
                this._capturedEventId = this.connect('captured-event',
                    this._onCapturedEvent.bind(this));
            }
        }
    }

    _stopAltCapture() {
        if (this._capturedEventId > 0) {
            this.disconnect(this._capturedEventId);
            this._capturedEventId = 0;
        }
        this._rebootButton = null;
        this._rebootButtonAlt = null;
    }

    close(skipSignal) {
        super.close();

        if (!skipSignal)
            this._dbusImpl.emit_signal('Closed', null);
    }

    cancel() {
        this._stopTimer();
        this._stopAltCapture();
        this._dbusImpl.emit_signal('Canceled', null);
        this.close();
    }

    _confirmRebootToBootLoaderMenu() {
        this._loginManager.setRebootToBootLoaderMenu();
        this._confirm('ConfirmedReboot').catch(logError);
    }

    async _confirm(signal) {
        if (this._checkBox.visible) {
            // Trigger the offline update as requested
            if (this._checkBox.checked) {
                switch (signal) {
                case 'ConfirmedReboot':
                    await this._triggerOfflineUpdateReboot();
                    break;
                case 'ConfirmedShutdown':
                    // To actually trigger the offline update, we need to
                    // reboot to do the upgrade. When the upgrade is complete,
                    // the computer will shut down automatically.
                    signal = 'ConfirmedReboot';
                    await this._triggerOfflineUpdateShutdown();
                    break;
                default:
                    break;
                }
            } else {
                await this._triggerOfflineUpdateCancel();
            }
        }

        this._fadeOutDialog();
        this._stopTimer();
        this._stopAltCapture();
        this._dbusImpl.emit_signal(signal, null);
    }

    _onOpened() {
        this._sync();
    }

    async _triggerOfflineUpdateReboot() {
        // Handle this gracefully if PackageKit is not available.
        if (!this._pkOfflineProxy)
            return;

        try {
            await this._pkOfflineProxy.TriggerAsync('reboot');
        } catch (error) {
            log(error.message);
        }
    }

    async _triggerOfflineUpdateShutdown() {
        // Handle this gracefully if PackageKit is not available.
        if (!this._pkOfflineProxy)
            return;

        try {
            await this._pkOfflineProxy.TriggerAsync('power-off');
        } catch (error) {
            log(error.message);
        }
    }

    async _triggerOfflineUpdateCancel() {
        // Handle this gracefully if PackageKit is not available.
        if (!this._pkOfflineProxy)
            return;

        try {
            await this._pkOfflineProxy.CancelAsync();
        } catch (error) {
            log(error.message);
        }
    }

    _startTimer() {
        let startTime = GLib.get_monotonic_time();
        this._secondsLeft = this._totalSecondsToStayOpen;

        this._timerId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
            let currentTime = GLib.get_monotonic_time();
            let secondsElapsed = (currentTime - startTime) / 1000000;

            this._secondsLeft = this._totalSecondsToStayOpen - secondsElapsed;
            if (this._secondsLeft > 0) {
                this._sync();
                return GLib.SOURCE_CONTINUE;
            }

            let dialogContent = DialogContent[this._type];
            let button = dialogContent.confirmButtons[dialogContent.confirmButtons.length - 1];
            this._confirm(button.signal).catch(logError);
            this._timerId = 0;

            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(this._timerId, '[gnome-shell] this._confirm');
    }

    _stopTimer() {
        if (this._timerId > 0) {
            GLib.source_remove(this._timerId);
            this._timerId = 0;
        }

        this._secondsLeft = 0;
    }

    _onInhibitorLoaded(inhibitor) {
        if (!this._applications.includes(inhibitor)) {
            // Stale inhibitor
            return;
        }

        let app = findAppFromInhibitor(inhibitor);
        const [flags] = app ? inhibitor.GetFlagsSync() : [0];

        if (app && flags & GnomeSession.InhibitFlags.LOGOUT) {
            let [description] = inhibitor.GetReasonSync();
            let listItem = new Dialog.ListSectionItem({
                icon_actor: app.create_icon_texture(_ITEM_ICON_SIZE),
                title: app.get_name(),
                description,
            });
            this._applicationSection.list.add_child(listItem);
        } else {
            // inhibiting app is a service (not an application) or is not
            // inhibiting logout/shutdown
            this._applications.splice(this._applications.indexOf(inhibitor), 1);
        }

        this._sync();
    }

    async _loadSessions() {
        let sessionId = GLib.getenv('XDG_SESSION_ID');
        if (!sessionId) {
            const currentSessionProxy = await this._loginManager.getCurrentSessionProxy();
            sessionId = currentSessionProxy.Id;
            log(`endSessionDialog: No XDG_SESSION_ID, fetched from logind: ${sessionId}`);
        }

        const sessions = await this._loginManager.listSessions();
        for (const [id_, uid_, userName, seat_, sessionPath] of sessions) {
            let proxy = new LogindSession(Gio.DBus.system, 'org.freedesktop.login1', sessionPath);

            if (proxy.Class !== 'user')
                continue;

            if (proxy.State === 'closing')
                continue;

            if (proxy.Id === sessionId)
                continue;

            const session = {
                user: this._userManager.get_user(userName),
                username: userName,
                type: proxy.Type,
                remote: proxy.Remote,
            };
            const nSessions = this._sessions.push(session);

            let userAvatar = new UserWidget.Avatar(session.user, {
                iconSize: _ITEM_ICON_SIZE,
            });
            userAvatar.update();

            const displayUserName =
                session.user.get_real_name() ?? session.username;

            let userLabelText;
            if (session.remote)
                /* Translators: Remote here refers to a remote session, like a ssh login */
                userLabelText = _('%s (remote)').format(displayUserName);
            else if (session.type === 'tty')
                /* Translators: Console here refers to a tty like a VT console */
                userLabelText = _('%s (console)').format(displayUserName);
            else
                userLabelText = userName;

            let listItem = new Dialog.ListSectionItem({
                icon_actor: userAvatar,
                title: userLabelText,
            });
            this._sessionSection.list.add_child(listItem);

            // limit the number of entries
            if (nSessions === MAX_USERS_IN_SESSION_DIALOG)
                break;
        }

        this._sync();
    }

    async _getUpdateInfo() {
        const connection = this._pkOfflineProxy.get_connection();
        const reply = await connection.call(
            this._pkOfflineProxy.g_name,
            this._pkOfflineProxy.g_object_path,
            'org.freedesktop.DBus.Properties',
            'GetAll',
            new GLib.Variant('(s)', [this._pkOfflineProxy.g_interface_name]),
            null,
            Gio.DBusCallFlags.NONE,
            -1,
            null);
        const [info] = reply.recursiveUnpack();
        return info;
    }

    async OpenAsync(parameters, invocation) {
        let [type, timestamp_, totalSecondsToStayOpen, inhibitorObjectPaths] = parameters;
        this._totalSecondsToStayOpen = totalSecondsToStayOpen;
        this._type = type;

        try {
            this._updateInfo = await this._getUpdateInfo();
        } catch (e) {
            if (this._pkOfflineProxy !== null)
                log(`Failed to get update info from PackageKit: ${e.message}`);

            this._updateInfo = {
                UpdateTriggered: false,
                UpdatePrepared: false,
                UpgradeTriggered: false,
                PreparedUpgrade: {},
            };
        }

        // Only consider updates and upgrades if PackageKit is available.
        if (this._pkOfflineProxy && this._type === DialogType.RESTART) {
            if (this._updateInfo.UpdateTriggered)
                this._type = DialogType.UPDATE_RESTART;
            else if (this._updateInfo.UpgradeTriggered)
                this._type = DialogType.UPGRADE_RESTART;
        }

        this._applications = [];
        this._applicationSection.list.destroy_all_children();

        this._sessions = [];
        this._sessionSection.list.destroy_all_children();

        if (!(this._type in DialogContent)) {
            invocation.return_error_literal(ModalDialogErrors,
                ModalDialogError.UNKNOWN_TYPE,
                'Unknown dialog type requested');
            return;
        }

        let dialogContent = DialogContent[this._type];

        for (let i = 0; i < inhibitorObjectPaths.length; i++) {
            let inhibitor = new GnomeSession.Inhibitor(inhibitorObjectPaths[i], proxy => {
                this._onInhibitorLoaded(proxy);
            });

            this._applications.push(inhibitor);
        }

        if (dialogContent.showOtherSessions)
            this._loadSessions().catch(logError);

        let updatesAllowed = this._updatesPermission && this._updatesPermission.allowed;

        _setCheckBoxLabel(this._checkBox, dialogContent.checkBoxText || '');
        this._checkBox.visible = dialogContent.checkBoxText && this._updateInfo.UpdatePrepared && updatesAllowed;

        if (this._type === DialogType.UPGRADE_RESTART)
            this._checkBox.checked = this._checkBox.visible && this._updateInfo.UpdateTriggered && !this._isDischargingBattery();
        else
            this._checkBox.checked = this._checkBox.visible && !this._isBatteryLow();

        this._batteryWarning.visible = this._shouldShowLowBatteryWarning(dialogContent);

        this._updateButtons();

        if (!this.open()) {
            invocation.return_error_literal(
                ModalDialogError.GRAB_FAILED,
                'Cannot grab pointer and keyboard');
            return;
        }

        if (!dialogContent.disableTimer)
            this._startTimer();

        this._sync();

        let signalId = this.connect('opened', () => {
            invocation.return_value(null);
            this.disconnect(signalId);
        });
    }

    Close(_parameters, _invocation) {
        this.close();
    }
});
(uuay)windowPreview.js�U// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as DND from './dnd.js';
import * as OverviewControls from './overviewControls.js';

const WINDOW_DND_SIZE = 256;

const WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 750;
const WINDOW_OVERLAY_FADE_TIME = 200;

const WINDOW_SCALE_TIME = 200;
const WINDOW_ACTIVE_SIZE_INC = 5; // in each direction

const DRAGGING_WINDOW_OPACITY = 100;

const ICON_SIZE = 64;
const ICON_OVERLAP = 0.7;

const ICON_TITLE_SPACING = 6;

export const WindowPreview = GObject.registerClass({
    Properties: {
        'overlay-enabled': GObject.ParamSpec.boolean(
            'overlay-enabled', 'overlay-enabled', 'overlay-enabled',
            GObject.ParamFlags.READWRITE,
            true),
    },
    Signals: {
        'drag-begin': {},
        'drag-cancelled': {},
        'drag-end': {},
        'selected': {param_types: [GObject.TYPE_UINT]},
        'show-chrome': {},
        'size-changed': {},
    },
}, class WindowPreview extends Shell.WindowPreview {
    _init(metaWindow, workspace, overviewAdjustment) {
        this.metaWindow = metaWindow;
        this.metaWindow._delegate = this;
        this._windowActor = metaWindow.get_compositor_private();
        this._workspace = workspace;
        this._overviewAdjustment = overviewAdjustment;

        super._init({
            reactive: true,
            can_focus: true,
            accessible_role: Atk.Role.PUSH_BUTTON,
            offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY,
        });

        const windowContainer = new Clutter.Actor({
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
        });
        this.window_container = windowContainer;

        windowContainer.connect('notify::scale-x',
            () => this._adjustOverlayOffsets());
        // gjs currently can't handle setting an actors layout manager during
        // the initialization of the actor if that layout manager keeps track
        // of its container, so set the layout manager after creating the
        // container
        windowContainer.layout_manager = new Shell.WindowPreviewLayout();
        this.add_child(windowContainer);

        this._addWindow(metaWindow);

        this._delegate = this;

        this._stackAbove = null;

        this._cachedBoundingBox = {
            x: windowContainer.layout_manager.bounding_box.x1,
            y: windowContainer.layout_manager.bounding_box.y1,
            width: windowContainer.layout_manager.bounding_box.get_width(),
            height: windowContainer.layout_manager.bounding_box.get_height(),
        };

        windowContainer.layout_manager.connect(
            'notify::bounding-box', layout => {
                this._cachedBoundingBox = {
                    x: layout.bounding_box.x1,
                    y: layout.bounding_box.y1,
                    width: layout.bounding_box.get_width(),
                    height: layout.bounding_box.get_height(),
                };

                // A bounding box of 0x0 means all windows were removed
                if (layout.bounding_box.get_area() > 0)
                    this.emit('size-changed');
            });

        this._windowActor.connectObject('destroy', () => this.destroy(), this);

        this._updateAttachedDialogs();

        this.connect('destroy', this._onDestroy.bind(this));

        this._draggable = DND.makeDraggable(this, {
            restoreOnSuccess: true,
            dragActorMaxSize: WINDOW_DND_SIZE,
            dragActorOpacity: DRAGGING_WINDOW_OPACITY,
        });
        this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
        this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
        this._draggable.connect('drag-end', this._onDragEnd.bind(this));
        this.inDrag = false;

        let clickAction = new Clutter.ClickAction();
        clickAction.connect('clicked', () => this._activate());
        clickAction.connect('long-press', (action, actor, state) => {
            if (state === Clutter.LongPressState.ACTIVATE)
                this.showOverlay(true);
            return true;
        });

        this._draggable.addClickAction(clickAction);

        this._overlayEnabled = true;
        this._overlayShown = false;
        this._closeRequested = false;
        this._idleHideOverlayId = 0;

        const tracker = Shell.WindowTracker.get_default();
        const app = tracker.get_window_app(this.metaWindow);
        this._icon = app.create_icon_texture(ICON_SIZE);
        this._icon.add_style_class_name('icon-dropshadow');
        this._icon.set({
            reactive: true,
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
        });
        this._icon.add_constraint(new Clutter.BindConstraint({
            source: windowContainer,
            coordinate: Clutter.BindCoordinate.POSITION,
        }));
        this._icon.add_constraint(new Clutter.AlignConstraint({
            source: windowContainer,
            align_axis: Clutter.AlignAxis.X_AXIS,
            factor: 0.5,
        }));
        this._icon.add_constraint(new Clutter.AlignConstraint({
            source: windowContainer,
            align_axis: Clutter.AlignAxis.Y_AXIS,
            pivot_point: new Graphene.Point({x: -1, y: ICON_OVERLAP}),
            factor: 1,
        }));

        const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
        this._title = new St.Label({
            visible: false,
            style_class: 'window-caption',
            text: this._getCaption(),
            reactive: true,
        });
        this._title.clutter_text.single_line_mode = true;
        this._title.add_constraint(new Clutter.BindConstraint({
            source: windowContainer,
            coordinate: Clutter.BindCoordinate.X,
        }));
        const iconBottomOverlap = ICON_SIZE * (1 - ICON_OVERLAP);
        this._title.add_constraint(new Clutter.BindConstraint({
            source: windowContainer,
            coordinate: Clutter.BindCoordinate.Y,
            offset: scaleFactor * (iconBottomOverlap + ICON_TITLE_SPACING),
        }));
        this._title.add_constraint(new Clutter.AlignConstraint({
            source: windowContainer,
            align_axis: Clutter.AlignAxis.X_AXIS,
            factor: 0.5,
        }));
        this._title.add_constraint(new Clutter.AlignConstraint({
            source: windowContainer,
            align_axis: Clutter.AlignAxis.Y_AXIS,
            pivot_point: new Graphene.Point({x: -1, y: 0}),
            factor: 1,
        }));
        this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
        this.label_actor = this._title;
        this.metaWindow.connectObject(
            'notify::title', () => (this._title.text = this._getCaption()),
            this);

        const layout = Meta.prefs_get_button_layout();
        this._closeButtonSide =
            layout.left_buttons.includes(Meta.ButtonFunction.CLOSE)
                ? St.Side.LEFT : St.Side.RIGHT;

        this._closeButton = new St.Button({
            visible: false,
            style_class: 'window-close',
            icon_name: 'preview-close-symbolic',
        });
        this._closeButton.add_constraint(new Clutter.BindConstraint({
            source: windowContainer,
            coordinate: Clutter.BindCoordinate.POSITION,
        }));
        this._closeButton.add_constraint(new Clutter.AlignConstraint({
            source: windowContainer,
            align_axis: Clutter.AlignAxis.X_AXIS,
            pivot_point: new Graphene.Point({x: 0.5, y: -1}),
            factor: this._closeButtonSide === St.Side.LEFT ? 0 : 1,
        }));
        this._closeButton.add_constraint(new Clutter.AlignConstraint({
            source: windowContainer,
            align_axis: Clutter.AlignAxis.Y_AXIS,
            pivot_point: new Graphene.Point({x: -1, y: 0.5}),
            factor: 0,
        }));
        this._closeButton.connect('clicked', () => this._deleteAll());

        this.add_child(this._title);
        this.add_child(this._icon);
        this.add_child(this._closeButton);

        this._overviewAdjustment.connectObject(
            'notify::value', () => this._updateIconScale(), this);
        this._updateIconScale();

        this.connect('notify::realized', () => {
            if (!this.realized)
                return;

            this._title.ensure_style();
            this._icon.ensure_style();
        });
    }

    _updateIconScale() {
        const {ControlsState} = OverviewControls;
        const {currentState, initialState, finalState} =
            this._overviewAdjustment.getStateTransitionParams();
        const visible =
            initialState === ControlsState.WINDOW_PICKER ||
            finalState === ControlsState.WINDOW_PICKER;
        const scale = visible
            ? 1 - Math.abs(ControlsState.WINDOW_PICKER - currentState) : 0;

        this._icon.set({
            scale_x: scale,
            scale_y: scale,
        });
    }

    _windowCanClose() {
        return this.metaWindow.can_close() &&
               !this._hasAttachedDialogs();
    }

    _getCaption() {
        if (this.metaWindow.title)
            return this.metaWindow.title;

        let tracker = Shell.WindowTracker.get_default();
        let app = tracker.get_window_app(this.metaWindow);
        return app.get_name();
    }

    overlapHeights() {
        const [, titleHeight] = this._title.get_preferred_height(-1);

        const topOverlap = 0;
        const bottomOverlap = ICON_TITLE_SPACING + titleHeight;

        return [topOverlap, bottomOverlap];
    }

    chromeHeights() {
        const [, closeButtonHeight] = this._closeButton.get_preferred_height(-1);
        const [, iconHeight] = this._icon.get_preferred_height(-1);
        const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
        const activeExtraSize = WINDOW_ACTIVE_SIZE_INC * scaleFactor;

        const topOversize = closeButtonHeight / 2;
        const bottomOversize = (1 - ICON_OVERLAP) * iconHeight;

        return [
            topOversize + activeExtraSize,
            bottomOversize + activeExtraSize,
        ];
    }

    chromeWidths() {
        const [, closeButtonWidth] = this._closeButton.get_preferred_width(-1);
        const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
        const activeExtraSize = WINDOW_ACTIVE_SIZE_INC * scaleFactor;

        const leftOversize = this._closeButtonSide === St.Side.LEFT
            ? closeButtonWidth / 2
            : 0;
        const rightOversize = this._closeButtonSide === St.Side.LEFT
            ? 0
            : closeButtonWidth / 2;

        return [
            leftOversize + activeExtraSize,
            rightOversize  + activeExtraSize,
        ];
    }

    showOverlay(animate) {
        if (!this._overlayEnabled)
            return;

        if (this._overlayShown)
            return;

        this._overlayShown = true;
        this._restack();

        // If we're supposed to animate and an animation in our direction
        // is already happening, let that one continue
        const ongoingTransition = this._title.get_transition('opacity');
        if (animate &&
            ongoingTransition &&
            ongoingTransition.get_interval().peek_final_value() === 255)
            return;

        const toShow = this._windowCanClose()
            ? [this._title, this._closeButton]
            : [this._title];

        toShow.forEach(a => {
            a.opacity = 0;
            a.show();
            a.ease({
                opacity: 255,
                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        });

        const [width, height] = this.window_container.get_size();
        const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
        const activeExtraSize = WINDOW_ACTIVE_SIZE_INC * 2 * scaleFactor;
        const origSize = Math.max(width, height);
        const scale = (origSize + activeExtraSize) / origSize;

        this.window_container.ease({
            scale_x: scale,
            scale_y: scale,
            duration: animate ? WINDOW_SCALE_TIME : 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        this.emit('show-chrome');
    }

    hideOverlay(animate) {
        if (!this._overlayShown)
            return;

        this._overlayShown = false;
        this._restack();

        // If we're supposed to animate and an animation in our direction
        // is already happening, let that one continue
        const ongoingTransition = this._title.get_transition('opacity');
        if (animate &&
            ongoingTransition &&
            ongoingTransition.get_interval().peek_final_value() === 0)
            return;

        [this._title, this._closeButton].forEach(a => {
            a.opacity = 255;
            a.ease({
                opacity: 0,
                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => a.hide(),
            });
        });

        this.window_container.ease({
            scale_x: 1,
            scale_y: 1,
            duration: animate ? WINDOW_SCALE_TIME : 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    _adjustOverlayOffsets() {
        // Assume that scale-x and scale-y update always set
        // in lock-step; that allows us to not use separate
        // handlers for horizontal and vertical offsets
        const previewScale = this.window_container.scale_x;
        const [previewWidth, previewHeight] =
            this.window_container.allocation.get_size();

        const heightIncrease =
            Math.floor(previewHeight * (previewScale - 1) / 2);
        const widthIncrease =
            Math.floor(previewWidth * (previewScale - 1) / 2);

        const closeAlign = this._closeButtonSide === St.Side.LEFT ? -1 : 1;

        this._icon.translation_y = heightIncrease;
        this._title.translation_y = heightIncrease;
        this._closeButton.set({
            translation_x: closeAlign * widthIncrease,
            translation_y: -heightIncrease,
        });
    }

    _addWindow(metaWindow) {
        const clone = this.window_container.layout_manager.add_window(metaWindow);
        if (!clone)
            return;

        // We expect this to be used for all interaction rather than
        // the ClutterClone; as the former is reactive and the latter
        // is not, this just works for most cases. However, for DND all
        // actors are picked, so DND operations would operate on the clone.
        // To avoid this, we hide it from pick.
        Shell.util_set_hidden_from_pick(clone, true);
    }

    vfunc_has_overlaps() {
        return this._hasAttachedDialogs() ||
            this._icon.visible ||
            this._closeButton.visible;
    }

    _deleteAll() {
        const windows = this.window_container.layout_manager.get_windows();

        // Delete all windows, starting from the bottom-most (most-modal) one
        for (const window of windows.reverse())
            window.delete(global.get_current_time());

        this._closeRequested = true;
    }

    addDialog(win) {
        let parent = win.get_transient_for();
        while (parent.is_attached_dialog())
            parent = parent.get_transient_for();

        // Display dialog if it is attached to our metaWindow
        if (win.is_attached_dialog() && parent === this.metaWindow)
            this._addWindow(win);

        // The dialog popped up after the user tried to close the window,
        // assume it's a close confirmation and leave the overview
        if (this._closeRequested)
            this._activate();
    }

    _hasAttachedDialogs() {
        return this.window_container.layout_manager.get_windows().length > 1;
    }

    _updateAttachedDialogs() {
        let iter = win => {
            let actor = win.get_compositor_private();

            if (!actor)
                return false;
            if (!win.is_attached_dialog())
                return false;

            this._addWindow(win);
            win.foreach_transient(iter);
            return true;
        };
        this.metaWindow.foreach_transient(iter);
    }

    get boundingBox() {
        return {...this._cachedBoundingBox};
    }

    get windowCenter() {
        return {
            x: this._cachedBoundingBox.x + this._cachedBoundingBox.width / 2,
            y: this._cachedBoundingBox.y + this._cachedBoundingBox.height / 2,
        };
    }

    get overlayEnabled() {
        return this._overlayEnabled;
    }

    set overlayEnabled(enabled) {
        if (this._overlayEnabled === enabled)
            return;

        this._overlayEnabled = enabled;
        this.notify('overlay-enabled');

        if (!enabled)
            this.hideOverlay(false);
        else if (this['has-pointer'] || global.stage.key_focus === this)
            this.showOverlay(true);
    }

    // Find the actor just below us, respecting reparenting done by DND code
    _getActualStackAbove() {
        if (this._stackAbove == null)
            return null;

        if (this.inDrag) {
            if (this._stackAbove._delegate)
                return this._stackAbove._delegate._getActualStackAbove();
            else
                return null;
        } else {
            return this._stackAbove;
        }
    }

    setStackAbove(actor) {
        this._stackAbove = actor;
        if (this.inDrag)
            // We'll fix up the stack after the drag
            return;

        let parent = this.get_parent();
        let actualAbove = this._getActualStackAbove();
        if (actualAbove == null)
            parent.set_child_below_sibling(this, null);
        else
            parent.set_child_above_sibling(this, actualAbove);
    }

    _onDestroy() {
        this.metaWindow._delegate = null;
        this._delegate = null;
        this._destroyed = true;

        if (this._longPressLater) {
            const laters = global.compositor.get_laters();
            laters.remove(this._longPressLater);
            delete this._longPressLater;
        }

        if (this._idleHideOverlayId > 0) {
            GLib.source_remove(this._idleHideOverlayId);
            this._idleHideOverlayId = 0;
        }

        if (this.inDrag) {
            this.emit('drag-end');
            this.inDrag = false;
        }
    }

    _activate() {
        this.emit('selected', global.get_current_time());
    }

    vfunc_enter_event(event) {
        this.showOverlay(true);
        return super.vfunc_enter_event(event);
    }

    vfunc_leave_event(event) {
        if (this._destroyed)
            return super.vfunc_leave_event(event);

        if ((event.get_flags() & Clutter.EventFlags.FLAG_GRAB_NOTIFY) !== 0 &&
            global.stage.get_grab_actor() === this._closeButton)
            return super.vfunc_leave_event(event);

        if (this._idleHideOverlayId > 0)
            GLib.source_remove(this._idleHideOverlayId);

        this._idleHideOverlayId = GLib.timeout_add(
            GLib.PRIORITY_DEFAULT,
            WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT, () => {
                if (this._closeButton['has-pointer'] ||
                    this._icon['has-pointer'] ||
                    this._title['has-pointer'])
                    return GLib.SOURCE_CONTINUE;

                if (!this['has-pointer'])
                    this.hideOverlay(true);

                this._idleHideOverlayId = 0;
                return GLib.SOURCE_REMOVE;
            });

        GLib.Source.set_name_by_id(this._idleHideOverlayId, '[gnome-shell] this._idleHideOverlayId');

        return super.vfunc_leave_event(event);
    }

    vfunc_key_focus_in() {
        super.vfunc_key_focus_in();
        this.showOverlay(true);
    }

    vfunc_key_focus_out() {
        super.vfunc_key_focus_out();

        if (global.stage.get_grab_actor() !== this._closeButton)
            this.hideOverlay(true);
    }

    vfunc_key_press_event(event) {
        let symbol = event.get_key_symbol();
        let isEnter = symbol === Clutter.KEY_Return || symbol === Clutter.KEY_KP_Enter;
        if (isEnter) {
            this._activate();
            return true;
        }

        return super.vfunc_key_press_event(event);
    }

    _restack() {
        // We may not have a parent if DnD completed successfully, in
        // which case our clone will shortly be destroyed and replaced
        // with a new one on the target workspace.
        const parent = this.get_parent();
        if (parent !== null) {
            if (this._overlayShown)
                parent.set_child_above_sibling(this, null);
            else if (this._stackAbove === null)
                parent.set_child_below_sibling(this, null);
            else if (!this._stackAbove._overlayShown)
                parent.set_child_above_sibling(this, this._stackAbove);
        }
    }

    _onDragBegin(_draggable, _time) {
        this.inDrag = true;
        this.hideOverlay(false);
        this.emit('drag-begin');
    }

    handleDragOver(source, actor, x, y, time) {
        return this._workspace.handleDragOver(source, actor, x, y, time);
    }

    acceptDrop(source, actor, x, y, time) {
        return this._workspace.acceptDrop(source, actor, x, y, time);
    }

    _onDragCancelled(_draggable, _time) {
        this.emit('drag-cancelled');
    }

    _onDragEnd(_draggable, _time, _snapback) {
        this.inDrag = false;

        this._restack();

        if (this['has-pointer'])
            this.showOverlay(true);

        this.emit('drag-end');
    }
});
(uuay)inhibitShortcutsDialog.js�import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Dialog from './dialog.js';
import * as ModalDialog from './modalDialog.js';
import * as PermissionStore from '../misc/permissionStore.js';

const APP_ALLOWLIST = ['org.gnome.Settings.desktop'];
const APP_PERMISSIONS_TABLE = 'gnome';
const APP_PERMISSIONS_ID = 'shortcuts-inhibitor';
const GRANTED = 'GRANTED';
const DENIED = 'DENIED';

const DialogResponse = Meta.InhibitShortcutsDialogResponse;

export const InhibitShortcutsDialog = GObject.registerClass({
    Implements: [Meta.InhibitShortcutsDialog],
    Properties: {
        'window': GObject.ParamSpec.override('window', Meta.InhibitShortcutsDialog),
    },
}, class InhibitShortcutsDialog extends GObject.Object {
    _init(window) {
        super._init();
        this._window = window;

        this._dialog = new ModalDialog.ModalDialog();
        this._buildLayout();
    }

    get window() {
        return this._window;
    }

    set window(window) {
        this._window = window;
    }

    get _app() {
        let windowTracker = Shell.WindowTracker.get_default();
        return windowTracker.get_window_app(this._window);
    }

    _shouldUsePermStore() {
        return this._app && !this._app.is_window_backed();
    }

    async _saveToPermissionStore(grant) {
        if (!this._shouldUsePermStore() || this._permStore == null)
            return;

        try {
            await this._permStore.SetPermissionAsync(APP_PERMISSIONS_TABLE,
                true,
                APP_PERMISSIONS_ID,
                this._app.get_id(),
                [grant]);
        } catch (error) {
            log(error.message);
        }
    }

    _buildLayout() {
        const name = this._app?.get_name() ?? this._window.title;

        let content = new Dialog.MessageDialogContent({
            title: _('Allow inhibiting shortcuts'),
            description: name
                /* Translators: %s is an application name like "Settings" */
                ? _('The app %s wants to inhibit shortcuts').format(name)
                : _('An app wants to inhibit shortcuts'),
        });

        const restoreAccel = Meta.prefs_get_keybinding_label('restore-shortcuts');
        if (restoreAccel) {
            let restoreLabel = new St.Label({
                /* Translators: %s is a keyboard shortcut like "Super+x" */
                text: _('You can restore shortcuts by pressing %s.').format(restoreAccel),
                style_class: 'message-dialog-description',
            });
            restoreLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
            restoreLabel.clutter_text.line_wrap = true;
            content.add_child(restoreLabel);
        }

        this._dialog.contentLayout.add_child(content);

        this._dialog.addButton({
            label: _('Deny'),
            action: () => {
                this._saveToPermissionStore(DENIED);
                this._emitResponse(DialogResponse.DENY);
            },
            key: Clutter.KEY_Escape,
        });

        this._dialog.addButton({
            label: _('Allow'),
            action: () => {
                this._saveToPermissionStore(GRANTED);
                this._emitResponse(DialogResponse.ALLOW);
            },
            default: true,
        });
    }

    _emitResponse(response) {
        this.emit('response', response);
        this._dialog.close();
    }

    vfunc_show() {
        if (this._app && APP_ALLOWLIST.includes(this._app.get_id())) {
            this._emitResponse(DialogResponse.ALLOW);
            return;
        }

        if (!this._shouldUsePermStore()) {
            this._dialog.open();
            return;
        }

        /* Check with the permission store */
        let appId = this._app.get_id();
        this._permStore = new PermissionStore.PermissionStore(async (proxy, error) => {
            if (error) {
                log(error.message);
                this._dialog.open();
                return;
            }

            try {
                const [permissions] = await this._permStore.LookupAsync(
                    APP_PERMISSIONS_TABLE, APP_PERMISSIONS_ID);

                if (permissions[appId] === undefined) // Not found
                    this._dialog.open();
                else if (permissions[appId][0] === GRANTED)
                    this._emitResponse(DialogResponse.ALLOW);
                else
                    this._emitResponse(DialogResponse.DENY);
            } catch (err) {
                this._dialog.open();
                log(err.message);
            }
        });
    }

    vfunc_hide() {
        this._dialog.close();
    }
});
(uuay)swipeTracker.js-g// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Mtk from 'gi://Mtk';

import * as Main from './main.js';
import * as Params from '../misc/params.js';

// FIXME: ideally these values matches physical touchpad size. We can get the
// correct values for gnome-shell specifically, since mutter uses libinput
// directly, but GTK apps cannot get it, so use an arbitrary value so that
// it's consistent with apps.
const TOUCHPAD_BASE_HEIGHT = 300;
const TOUCHPAD_BASE_WIDTH = 400;

const EVENT_HISTORY_THRESHOLD_MS = 150;

const SCROLL_MULTIPLIER = 10;

const MIN_ANIMATION_DURATION = 100;
const MAX_ANIMATION_DURATION = 400;
const VELOCITY_THRESHOLD_TOUCH = 0.3;
const VELOCITY_THRESHOLD_TOUCHPAD = 0.6;
const DECELERATION_TOUCH = 0.998;
const DECELERATION_TOUCHPAD = 0.997;
const VELOCITY_CURVE_THRESHOLD = 2;
const DECELERATION_PARABOLA_MULTIPLIER = 0.35;
const DRAG_THRESHOLD_DISTANCE = 16;

// Derivative of easeOutCubic at t=0
const DURATION_MULTIPLIER = 3;
const ANIMATION_BASE_VELOCITY = 0.002;
const EPSILON = 0.005;

const GESTURE_FINGER_COUNT = 3;

/** @enum {number} */
const State = {
    NONE: 0,
    SCROLLING: 1,
};

const TouchpadState = {
    NONE: 0,
    PENDING: 1,
    HANDLING: 2,
    IGNORED: 3,
};

const EventHistory = class {
    constructor() {
        this.reset();
    }

    reset() {
        this._data = [];
    }

    trim(time) {
        const thresholdTime = time - EVENT_HISTORY_THRESHOLD_MS;
        const index = this._data.findIndex(r => r.time >= thresholdTime);

        this._data.splice(0, index);
    }

    append(time, delta) {
        this.trim(time);

        this._data.push({time, delta});
    }

    calculateVelocity() {
        if (this._data.length < 2)
            return 0;

        const firstTime = this._data[0].time;
        const lastTime = this._data[this._data.length - 1].time;

        if (firstTime === lastTime)
            return 0;

        const totalDelta = this._data.slice(1).map(a => a.delta).reduce((a, b) => a + b);
        const period = lastTime - firstTime;

        return totalDelta / period;
    }
};

const TouchpadSwipeGesture = GObject.registerClass({
    Properties: {
        'enabled': GObject.ParamSpec.boolean(
            'enabled', 'enabled', 'enabled',
            GObject.ParamFlags.READWRITE,
            true),
        'orientation': GObject.ParamSpec.enum(
            'orientation', 'orientation', 'orientation',
            GObject.ParamFlags.READWRITE,
            Clutter.Orientation, Clutter.Orientation.HORIZONTAL),
    },
    Signals: {
        'begin':  {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
        'update': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
        'end':    {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE]},
    },
}, class TouchpadSwipeGesture extends GObject.Object {
    _init(allowedModes) {
        super._init();
        this._allowedModes = allowedModes;
        this._state = TouchpadState.NONE;
        this._cumulativeX = 0;
        this._cumulativeY = 0;
        this._touchpadSettings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.peripherals.touchpad',
        });

        global.stage.connectObject(
            'captured-event::touchpad', this._handleEvent.bind(this), this);
    }

    _handleEvent(actor, event) {
        if (event.type() !== Clutter.EventType.TOUCHPAD_SWIPE)
            return Clutter.EVENT_PROPAGATE;

        if (event.get_gesture_phase() === Clutter.TouchpadGesturePhase.BEGIN)
            this._state = TouchpadState.NONE;

        if (event.get_touchpad_gesture_finger_count() !== GESTURE_FINGER_COUNT)
            return Clutter.EVENT_PROPAGATE;

        if ((this._allowedModes & Main.actionMode) === 0)
            return Clutter.EVENT_PROPAGATE;

        if (!this.enabled)
            return Clutter.EVENT_PROPAGATE;

        if (this._state === TouchpadState.IGNORED)
            return Clutter.EVENT_PROPAGATE;

        let time = event.get_time();

        const [x, y] = event.get_coords();
        const [dx, dy] = event.get_gesture_motion_delta_unaccelerated();

        if (this._state === TouchpadState.NONE) {
            if (dx === 0 && dy === 0)
                return Clutter.EVENT_PROPAGATE;

            this._cumulativeX = 0;
            this._cumulativeY = 0;
            this._state = TouchpadState.PENDING;
        }

        if (this._state === TouchpadState.PENDING) {
            this._cumulativeX += dx;
            this._cumulativeY += dy;

            const cdx = this._cumulativeX;
            const cdy = this._cumulativeY;
            const distance = Math.sqrt(cdx * cdx + cdy * cdy);

            if (distance >= DRAG_THRESHOLD_DISTANCE) {
                const gestureOrientation = Math.abs(cdx) > Math.abs(cdy)
                    ? Clutter.Orientation.HORIZONTAL
                    : Clutter.Orientation.VERTICAL;

                this._cumulativeX = 0;
                this._cumulativeY = 0;

                if (gestureOrientation === this.orientation) {
                    this._state = TouchpadState.HANDLING;
                    this.emit('begin', time, x, y);
                } else {
                    this._state = TouchpadState.IGNORED;
                    return Clutter.EVENT_PROPAGATE;
                }
            } else {
                return Clutter.EVENT_PROPAGATE;
            }
        }

        const vertical = this.orientation === Clutter.Orientation.VERTICAL;
        let delta = vertical ? dy : dx;
        const distance = vertical ? TOUCHPAD_BASE_HEIGHT : TOUCHPAD_BASE_WIDTH;

        switch (event.get_gesture_phase()) {
        case Clutter.TouchpadGesturePhase.BEGIN:
        case Clutter.TouchpadGesturePhase.UPDATE:
            if (this._touchpadSettings.get_boolean('natural-scroll'))
                delta = -delta;

            this.emit('update', time, delta, distance);
            break;

        case Clutter.TouchpadGesturePhase.END:
        case Clutter.TouchpadGesturePhase.CANCEL:
            this.emit('end', time, distance);
            this._state = TouchpadState.NONE;
            break;
        }

        return this._state === TouchpadState.HANDLING
            ? Clutter.EVENT_STOP
            : Clutter.EVENT_PROPAGATE;
    }

    destroy() {
        global.stage.disconnectObject(this);
    }
});

const TouchSwipeGesture = GObject.registerClass({
    Properties: {
        'distance': GObject.ParamSpec.double(
            'distance', 'distance', 'distance',
            GObject.ParamFlags.READWRITE,
            0, Infinity, 0),
        'orientation': GObject.ParamSpec.enum(
            'orientation', 'orientation', 'orientation',
            GObject.ParamFlags.READWRITE,
            Clutter.Orientation, Clutter.Orientation.HORIZONTAL),
    },
    Signals: {
        'begin':  {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
        'update': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
        'end':    {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE]},
        'cancel': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE]},
    },
}, class TouchSwipeGesture extends Clutter.GestureAction {
    _init(allowedModes, nTouchPoints, thresholdTriggerEdge) {
        super._init();
        this.set_n_touch_points(nTouchPoints);
        this.set_threshold_trigger_edge(thresholdTriggerEdge);

        this._allowedModes = allowedModes;
        this._distance = global.screen_height;
        this._lastPosition = 0;
    }

    get distance() {
        return this._distance;
    }

    set distance(distance) {
        if (this._distance === distance)
            return;

        this._distance = distance;
        this.notify('distance');
    }

    vfunc_gesture_prepare(actor) {
        if (!super.vfunc_gesture_prepare(actor))
            return false;

        if ((this._allowedModes & Main.actionMode) === 0)
            return false;

        let time = this.get_last_event(0).get_time();
        let [xPress, yPress] = this.get_press_coords(0);
        let [x, y] = this.get_motion_coords(0);
        const [xDelta, yDelta] = [x - xPress, y - yPress];
        const swipeOrientation = Math.abs(xDelta) > Math.abs(yDelta)
            ? Clutter.Orientation.HORIZONTAL : Clutter.Orientation.VERTICAL;

        if (swipeOrientation !== this.orientation)
            return false;

        this._lastPosition =
            this.orientation === Clutter.Orientation.VERTICAL ? y : x;

        this.emit('begin', time, xPress, yPress);
        return true;
    }

    vfunc_gesture_progress(_actor) {
        let [x, y] = this.get_motion_coords(0);
        let pos = this.orientation === Clutter.Orientation.VERTICAL ? y : x;

        let delta = pos - this._lastPosition;
        this._lastPosition = pos;

        let time = this.get_last_event(0).get_time();

        this.emit('update', time, -delta, this._distance);

        return true;
    }

    vfunc_gesture_end(_actor) {
        let time = this.get_last_event(0).get_time();

        this.emit('end', time, this._distance);
    }

    vfunc_gesture_cancel(_actor) {
        let time = Clutter.get_current_event_time();

        this.emit('cancel', time, this._distance);
    }
});

const ScrollGesture = GObject.registerClass({
    Properties: {
        'enabled': GObject.ParamSpec.boolean(
            'enabled', 'enabled', 'enabled',
            GObject.ParamFlags.READWRITE,
            true),
        'orientation': GObject.ParamSpec.enum(
            'orientation', 'orientation', 'orientation',
            GObject.ParamFlags.READWRITE,
            Clutter.Orientation, Clutter.Orientation.HORIZONTAL),
        'scroll-modifiers': GObject.ParamSpec.flags(
            'scroll-modifiers', 'scroll-modifiers', 'scroll-modifiers',
            GObject.ParamFlags.READWRITE,
            Clutter.ModifierType, 0),
    },
    Signals: {
        'begin':  {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
        'update': {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE]},
        'end':    {param_types: [GObject.TYPE_UINT, GObject.TYPE_DOUBLE]},
    },
}, class ScrollGesture extends GObject.Object {
    _init(actor, allowedModes) {
        super._init();
        this._allowedModes = allowedModes;
        this._began = false;
        this._enabled = true;

        actor.connect('scroll-event', this._handleEvent.bind(this));
    }

    get enabled() {
        return this._enabled;
    }

    set enabled(enabled) {
        if (this._enabled === enabled)
            return;

        this._enabled = enabled;
        this._began = false;

        this.notify('enabled');
    }

    canHandleEvent(event) {
        if (event.type() !== Clutter.EventType.SCROLL)
            return false;

        if (event.get_scroll_source() !== Clutter.ScrollSource.FINGER &&
            event.get_source_device().get_device_type() !== Clutter.InputDeviceType.TOUCHPAD_DEVICE)
            return false;

        if (!this.enabled)
            return false;

        if ((this._allowedModes & Main.actionMode) === 0)
            return false;

        if (!this._began && this.scrollModifiers !== 0 &&
            (event.get_state() & this.scrollModifiers) === 0)
            return false;

        return true;
    }

    _handleEvent(actor, event) {
        if (!this.canHandleEvent(event))
            return Clutter.EVENT_PROPAGATE;

        if (event.get_scroll_direction() !== Clutter.ScrollDirection.SMOOTH)
            return Clutter.EVENT_PROPAGATE;

        const vertical = this.orientation === Clutter.Orientation.VERTICAL;
        const distance = vertical ? TOUCHPAD_BASE_HEIGHT : TOUCHPAD_BASE_WIDTH;

        let time = event.get_time();
        let [dx, dy] = event.get_scroll_delta();
        if (dx === 0 && dy === 0) {
            this.emit('end', time, distance);
            this._began = false;
            return Clutter.EVENT_STOP;
        }

        if (!this._began) {
            let [x, y] = event.get_coords();
            this.emit('begin', time, x, y);
            this._began = true;
        }

        const delta = (vertical ? dy : dx) * SCROLL_MULTIPLIER;

        this.emit('update', time, delta, distance);

        return Clutter.EVENT_STOP;
    }
});

// USAGE:
//
// To correctly implement the gesture, there must be handlers for the following
// signals:
//
// begin(tracker, monitor)
//   The handler should check whether a deceleration animation is currently
//   running. If it is, it should stop the animation (without resetting
//   progress). Then it should call:
//   tracker.confirmSwipe(distance, snapPoints, currentProgress, cancelProgress)
//   If it's not called, the swipe would be ignored.
//   The parameters are:
//    * distance: the page size;
//    * snapPoints: an (sorted with ascending order) array of snap points;
//    * currentProgress: the current progress;
//    * cancelprogress: a non-transient value that would be used if the gesture
//      is cancelled.
//   If no animation was running, currentProgress and cancelProgress should be
//   same. The handler may set 'orientation' property here.
//
// update(tracker, progress)
//   The handler should set the progress to the given value.
//
// end(tracker, duration, endProgress)
//   The handler should animate the progress to endProgress. If endProgress is
//   0, it should do nothing after the animation, otherwise it should change the
//   state, e.g. change the current page or switch workspace.
//   NOTE: duration can be 0 in some cases, in this case it should finish
//   instantly.

/** A class for handling swipe gestures */
export const SwipeTracker = GObject.registerClass({
    Properties: {
        'enabled': GObject.ParamSpec.boolean(
            'enabled', 'enabled', 'enabled',
            GObject.ParamFlags.READWRITE,
            true),
        'orientation': GObject.ParamSpec.enum(
            'orientation', 'orientation', 'orientation',
            GObject.ParamFlags.READWRITE,
            Clutter.Orientation, Clutter.Orientation.HORIZONTAL),
        'distance': GObject.ParamSpec.double(
            'distance', 'distance', 'distance',
            GObject.ParamFlags.READWRITE,
            0, Infinity, 0),
        'allow-long-swipes': GObject.ParamSpec.boolean(
            'allow-long-swipes', 'allow-long-swipes', 'allow-long-swipes',
            GObject.ParamFlags.READWRITE,
            false),
        'scroll-modifiers': GObject.ParamSpec.flags(
            'scroll-modifiers', 'scroll-modifiers', 'scroll-modifiers',
            GObject.ParamFlags.READWRITE,
            Clutter.ModifierType, 0),
    },
    Signals: {
        'begin':  {param_types: [GObject.TYPE_UINT]},
        'update': {param_types: [GObject.TYPE_DOUBLE]},
        'end':    {param_types: [GObject.TYPE_UINT64, GObject.TYPE_DOUBLE]},
    },
}, class SwipeTracker extends GObject.Object {
    _init(actor, orientation, allowedModes, params) {
        super._init();
        params = Params.parse(params, {allowDrag: true, allowScroll: true});

        this.orientation = orientation;
        this._allowedModes = allowedModes;
        this._enabled = true;
        this._distance = global.screen_height;
        this._history = new EventHistory();
        this._reset();

        this._touchpadGesture = new TouchpadSwipeGesture(allowedModes);
        this._touchpadGesture.connect('begin', this._beginGesture.bind(this));
        this._touchpadGesture.connect('update', this._updateGesture.bind(this));
        this._touchpadGesture.connect('end', this._endTouchpadGesture.bind(this));
        this.bind_property('enabled', this._touchpadGesture, 'enabled', 0);
        this.bind_property('orientation', this._touchpadGesture, 'orientation',
            GObject.BindingFlags.SYNC_CREATE);

        this._touchGesture = new TouchSwipeGesture(allowedModes,
            GESTURE_FINGER_COUNT,
            Clutter.GestureTriggerEdge.AFTER);
        this._touchGesture.connect('begin', this._beginTouchSwipe.bind(this));
        this._touchGesture.connect('update', this._updateGesture.bind(this));
        this._touchGesture.connect('end', this._endTouchGesture.bind(this));
        this._touchGesture.connect('cancel', this._cancelTouchGesture.bind(this));
        this.bind_property('enabled', this._touchGesture, 'enabled', 0);
        this.bind_property('orientation', this._touchGesture, 'orientation',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('distance', this._touchGesture, 'distance', 0);
        global.stage.add_action_full('swipe', Clutter.EventPhase.CAPTURE, this._touchGesture);

        if (params.allowDrag) {
            this._dragGesture = new TouchSwipeGesture(allowedModes, 1,
                Clutter.GestureTriggerEdge.AFTER);
            this._dragGesture.connect('begin', this._beginGesture.bind(this));
            this._dragGesture.connect('update', this._updateGesture.bind(this));
            this._dragGesture.connect('end', this._endTouchGesture.bind(this));
            this._dragGesture.connect('cancel', this._cancelTouchGesture.bind(this));
            this.bind_property('enabled', this._dragGesture, 'enabled', 0);
            this.bind_property('orientation', this._dragGesture, 'orientation',
                GObject.BindingFlags.SYNC_CREATE);
            this.bind_property('distance', this._dragGesture, 'distance', 0);
            actor.add_action_full('drag', Clutter.EventPhase.CAPTURE, this._dragGesture);
        } else {
            this._dragGesture = null;
        }

        if (params.allowScroll) {
            this._scrollGesture = new ScrollGesture(actor, allowedModes);
            this._scrollGesture.connect('begin', this._beginGesture.bind(this));
            this._scrollGesture.connect('update', this._updateGesture.bind(this));
            this._scrollGesture.connect('end', this._endTouchpadGesture.bind(this));
            this.bind_property('enabled', this._scrollGesture, 'enabled', 0);
            this.bind_property('orientation', this._scrollGesture, 'orientation',
                GObject.BindingFlags.SYNC_CREATE);
            this.bind_property('scroll-modifiers',
                this._scrollGesture, 'scroll-modifiers', 0);
        } else {
            this._scrollGesture = null;
        }
    }

    /**
     * canHandleScrollEvent:
     * This function can be used to combine swipe gesture and mouse
     * scrolling.
     *
     * @param {Clutter.Event} scrollEvent an event to check
     * @returns {boolean} whether the event can be handled by the tracker
     */
    canHandleScrollEvent(scrollEvent) {
        if (!this.enabled || this._scrollGesture === null)
            return false;

        return this._scrollGesture.canHandleEvent(scrollEvent);
    }

    get enabled() {
        return this._enabled;
    }

    set enabled(enabled) {
        if (this._enabled === enabled)
            return;

        this._enabled = enabled;
        if (!enabled && this._state === State.SCROLLING)
            this._interrupt();
        this.notify('enabled');
    }

    get distance() {
        return this._distance;
    }

    set distance(distance) {
        if (this._distance === distance)
            return;

        this._distance = distance;
        this.notify('distance');
    }

    _reset() {
        this._state = State.NONE;

        this._snapPoints = [];
        this._initialProgress = 0;
        this._cancelProgress = 0;

        this._prevOffset = 0;
        this._progress = 0;

        this._cancelled = false;

        this._history.reset();
    }

    _interrupt() {
        this.emit('end', 0, this._cancelProgress);
        this._reset();
    }

    _beginTouchSwipe(gesture, time, x, y) {
        if (this._dragGesture)
            this._dragGesture.cancel();

        this._beginGesture(gesture, time, x, y);
    }

    _beginGesture(gesture, time, x, y) {
        if (this._state === State.SCROLLING)
            return;

        this._history.append(time, 0);

        const rect = new Mtk.Rectangle({x, y, width: 1, height: 1});
        let monitor = global.display.get_monitor_index_for_rect(rect);

        this.emit('begin', monitor);
    }

    _findClosestPoint(pos) {
        const distances = this._snapPoints.map(x => Math.abs(x - pos));
        const min = Math.min(...distances);
        return distances.indexOf(min);
    }

    _findNextPoint(pos) {
        return this._snapPoints.findIndex(p => p >= pos);
    }

    _findPreviousPoint(pos) {
        const reversedIndex = this._snapPoints.slice().reverse().findIndex(p => p <= pos);
        return this._snapPoints.length - 1 - reversedIndex;
    }

    _findPointForProjection(pos, velocity) {
        const initial = this._findClosestPoint(this._initialProgress);
        const prev = this._findPreviousPoint(pos);
        const next = this._findNextPoint(pos);

        if ((velocity > 0 ? prev : next) === initial)
            return velocity > 0 ? next : prev;

        return this._findClosestPoint(pos);
    }

    _getBounds(pos) {
        if (this.allowLongSwipes)
            return [this._snapPoints[0], this._snapPoints[this._snapPoints.length - 1]];

        const closest = this._findClosestPoint(pos);

        let prev, next;
        if (Math.abs(this._snapPoints[closest] - pos) < EPSILON) {
            prev = next = closest;
        } else {
            prev = this._findPreviousPoint(pos);
            next = this._findNextPoint(pos);
        }

        const lowerIndex = Math.max(prev - 1, 0);
        const upperIndex = Math.min(next + 1, this._snapPoints.length - 1);

        return [this._snapPoints[lowerIndex], this._snapPoints[upperIndex]];
    }

    _updateGesture(gesture, time, delta, distance) {
        if (this._state !== State.SCROLLING)
            return;

        if ((this._allowedModes & Main.actionMode) === 0 || !this.enabled) {
            this._interrupt();
            return;
        }

        if (this.orientation === Clutter.Orientation.HORIZONTAL &&
            Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
            delta = -delta;

        this._progress += delta / distance;
        this._history.append(time, delta);

        this._progress = Math.clamp(this._progress, ...this._getBounds(this._initialProgress));

        this.emit('update', this._progress);
    }

    _getEndProgress(velocity, distance, isTouchpad) {
        if (this._cancelled)
            return this._cancelProgress;

        const threshold = isTouchpad ? VELOCITY_THRESHOLD_TOUCHPAD : VELOCITY_THRESHOLD_TOUCH;

        if (Math.abs(velocity) < threshold)
            return this._snapPoints[this._findClosestPoint(this._progress)];

        const decel = isTouchpad ? DECELERATION_TOUCHPAD : DECELERATION_TOUCH;
        const slope = decel / (1.0 - decel) / 1000.0;

        let pos;
        if (Math.abs(velocity) > VELOCITY_CURVE_THRESHOLD) {
            const c = slope / 2 / DECELERATION_PARABOLA_MULTIPLIER;
            const x = Math.abs(velocity) - VELOCITY_CURVE_THRESHOLD + c;

            pos = slope * VELOCITY_CURVE_THRESHOLD +
                DECELERATION_PARABOLA_MULTIPLIER * x * x -
                DECELERATION_PARABOLA_MULTIPLIER * c * c;
        } else {
            pos = Math.abs(velocity) * slope;
        }

        pos = pos * Math.sign(velocity) + this._progress;
        pos = Math.clamp(pos, ...this._getBounds(this._initialProgress));

        const index = this._findPointForProjection(pos, velocity);

        return this._snapPoints[index];
    }

    _endTouchGesture(_gesture, time, distance) {
        this._endGesture(time, distance, false);
    }

    _endTouchpadGesture(_gesture, time, distance) {
        this._endGesture(time, distance, true);
    }

    _endGesture(time, distance, isTouchpad) {
        if (this._state !== State.SCROLLING)
            return;

        if ((this._allowedModes & Main.actionMode) === 0 || !this.enabled) {
            this._interrupt();
            return;
        }

        this._history.trim(time);

        let velocity = this._history.calculateVelocity();
        const endProgress = this._getEndProgress(velocity, distance, isTouchpad);

        velocity /= distance;

        if ((endProgress - this._progress) * velocity <= 0)
            velocity = ANIMATION_BASE_VELOCITY;

        const nPoints = Math.max(1, Math.ceil(Math.abs(this._progress - endProgress)));
        const maxDuration = MAX_ANIMATION_DURATION * Math.log2(1 + nPoints);

        let duration = Math.abs((this._progress - endProgress) / velocity * DURATION_MULTIPLIER);
        if (duration > 0)
            duration = Math.clamp(duration, MIN_ANIMATION_DURATION, maxDuration);

        this._reset();
        this.emit('end', duration, endProgress);
    }

    _cancelTouchGesture(_gesture, time, distance) {
        if (this._state !== State.SCROLLING)
            return;

        this._cancelled = true;
        this._endGesture(time, distance, false);
    }

    /**
     * Confirms a swipe. User has to call this in 'begin' signal handler,
     * otherwise the swipe wouldn't start. If there's an animation running,
     * it should be stopped first.
     *
     * `cancelProgress` must always be a snap point, or a value matching
     * some other non-transient state.
     *
     * @param {number} distance - swipe distance in pixels
     * @param {number[]} snapPoints - An array of snap points, sorted in ascending order
     * @param {number} currentProgress - initial progress value
     * @param {number} cancelProgress - the value to be used on cancelling
     */
    confirmSwipe(distance, snapPoints, currentProgress, cancelProgress) {
        this.distance = distance;
        this._snapPoints = snapPoints;
        this._initialProgress = currentProgress;
        this._progress = currentProgress;
        this._cancelProgress = cancelProgress;

        this._state = State.SCROLLING;
    }

    destroy() {
        if (this._touchpadGesture) {
            this._touchpadGesture.destroy();
            delete this._touchpadGesture;
        }

        if (this._touchGesture) {
            global.stage.remove_action(this._touchGesture);
            delete this._touchGesture;
        }
    }
});
(uuay)windowMenu.js�&// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*

import Clutter from 'gi://Clutter';
import Meta from 'gi://Meta';
import St from 'gi://St';

import * as BoxPointer from './boxpointer.js';
import * as Main from './main.js';
import * as PopupMenu from './popupMenu.js';
import * as Screenshot from './screenshot.js';

export class WindowMenu extends PopupMenu.PopupMenu {
    constructor(window, sourceActor) {
        super(sourceActor, 0, St.Side.TOP);

        this.actor.add_style_class_name('window-menu');

        Main.layoutManager.uiGroup.add_child(this.actor);
        this.actor.hide();

        this._buildMenu(window);
    }

    _buildMenu(window) {
        let type = window.get_window_type();

        let item;

        // Translators: entry in the window right click menu.
        item = this.addAction(_('Take Screenshot'), async () => {
            try {
                const actor = window.get_compositor_private();
                const content = actor.paint_to_content(null);
                const texture = content.get_texture();

                await Screenshot.captureScreenshot(texture, null, 1, null);
            } catch (e) {
                logError(e, 'Error capturing screenshot');
            }
        });

        item = this.addAction(_('Hide'), () => {
            window.minimize();
        });
        if (!window.can_minimize())
            item.setSensitive(false);

        if (window.get_maximized()) {
            item = this.addAction(_('Restore'), () => {
                window.unmaximize(Meta.MaximizeFlags.BOTH);
            });
        } else {
            item = this.addAction(_('Maximize'), () => {
                window.maximize(Meta.MaximizeFlags.BOTH);
            });
        }
        if (!window.can_maximize())
            item.setSensitive(false);

        item = this.addAction(_('Move'), event => {
            const device = event.get_device();
            const seat = device.get_seat();
            const deviceType = device.get_device_type();
            const pointer =
                deviceType === Clutter.InputDeviceType.POINTER_DEVICE ||
                deviceType === Clutter.InputDeviceType.TABLET_DEVICE ||
                deviceType === Clutter.InputDeviceType.PEN_DEVICE ||
                deviceType === Clutter.InputDeviceType.ERASER_DEVICE
                    ? device : seat.get_pointer();

            window.begin_grab_op(
                Meta.GrabOp.KEYBOARD_MOVING,
                pointer, null,
                event.get_time(),
                null);
        });
        if (!window.allows_move())
            item.setSensitive(false);

        item = this.addAction(_('Resize'), event => {
            const device = event.get_device();
            const seat = device.get_seat();
            const deviceType = device.get_device_type();
            const pointer =
                deviceType === Clutter.InputDeviceType.POINTER_DEVICE ||
                deviceType === Clutter.InputDeviceType.TABLET_DEVICE ||
                deviceType === Clutter.InputDeviceType.PEN_DEVICE ||
                deviceType === Clutter.InputDeviceType.ERASER_DEVICE
                    ? device : seat.get_pointer();

            window.begin_grab_op(
                Meta.GrabOp.KEYBOARD_RESIZING_UNKNOWN,
                pointer, null,
                event.get_time(),
                null);
        });
        if (!window.allows_resize())
            item.setSensitive(false);

        if (!window.titlebar_is_onscreen() && type !== Meta.WindowType.DOCK && type !== Meta.WindowType.DESKTOP) {
            this.addAction(_('Move Titlebar Onscreen'), () => {
                window.shove_titlebar_onscreen();
            });
        }

        item = this.addAction(_('Always on Top'), () => {
            if (window.is_above())
                window.unmake_above();
            else
                window.make_above();
        });
        if (window.is_above())
            item.setOrnament(PopupMenu.Ornament.CHECK);
        if (window.get_maximized() === Meta.MaximizeFlags.BOTH ||
            type === Meta.WindowType.DOCK ||
            type === Meta.WindowType.DESKTOP ||
            type === Meta.WindowType.SPLASHSCREEN)
            item.setSensitive(false);

        if (Main.sessionMode.hasWorkspaces &&
            (!Meta.prefs_get_workspaces_only_on_primary() ||
             window.is_on_primary_monitor())) {
            let isSticky = window.is_on_all_workspaces();

            item = this.addAction(_('Always on Visible Workspace'), () => {
                if (isSticky)
                    window.unstick();
                else
                    window.stick();
            });
            if (isSticky)
                item.setOrnament(PopupMenu.Ornament.CHECK);
            if (window.is_always_on_all_workspaces())
                item.setSensitive(false);

            if (!isSticky) {
                let workspace = window.get_workspace();
                if (workspace !== workspace.get_neighbor(Meta.MotionDirection.LEFT)) {
                    this.addAction(_('Move to Workspace Left'), () => {
                        let dir = Meta.MotionDirection.LEFT;
                        window.change_workspace(workspace.get_neighbor(dir));
                    });
                }
                if (workspace !== workspace.get_neighbor(Meta.MotionDirection.RIGHT)) {
                    this.addAction(_('Move to Workspace Right'), () => {
                        let dir = Meta.MotionDirection.RIGHT;
                        window.change_workspace(workspace.get_neighbor(dir));
                    });
                }
                if (workspace !== workspace.get_neighbor(Meta.MotionDirection.UP)) {
                    this.addAction(_('Move to Workspace Up'), () => {
                        let dir = Meta.MotionDirection.UP;
                        window.change_workspace(workspace.get_neighbor(dir));
                    });
                }
                if (workspace !== workspace.get_neighbor(Meta.MotionDirection.DOWN)) {
                    this.addAction(_('Move to Workspace Down'), () => {
                        let dir = Meta.MotionDirection.DOWN;
                        window.change_workspace(workspace.get_neighbor(dir));
                    });
                }
            }
        }

        let display = global.display;
        let nMonitors = display.get_n_monitors();
        let monitorIndex = window.get_monitor();
        if (nMonitors > 1 && monitorIndex >= 0) {
            this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

            let dir = Meta.DisplayDirection.UP;
            let upMonitorIndex =
                display.get_monitor_neighbor_index(monitorIndex, dir);
            if (upMonitorIndex !== -1) {
                this.addAction(_('Move to Monitor Up'), () => {
                    window.move_to_monitor(upMonitorIndex);
                });
            }

            dir = Meta.DisplayDirection.DOWN;
            let downMonitorIndex =
                display.get_monitor_neighbor_index(monitorIndex, dir);
            if (downMonitorIndex !== -1) {
                this.addAction(_('Move to Monitor Down'), () => {
                    window.move_to_monitor(downMonitorIndex);
                });
            }

            dir = Meta.DisplayDirection.LEFT;
            let leftMonitorIndex =
                display.get_monitor_neighbor_index(monitorIndex, dir);
            if (leftMonitorIndex !== -1) {
                this.addAction(_('Move to Monitor Left'), () => {
                    window.move_to_monitor(leftMonitorIndex);
                });
            }

            dir = Meta.DisplayDirection.RIGHT;
            let rightMonitorIndex =
                display.get_monitor_neighbor_index(monitorIndex, dir);
            if (rightMonitorIndex !== -1) {
                this.addAction(_('Move to Monitor Right'), () => {
                    window.move_to_monitor(rightMonitorIndex);
                });
            }
        }

        this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        item = this.addAction(_('Close'), event => {
            window.delete(event.get_time());
        });
        if (!window.can_close())
            item.setSensitive(false);
    }

    addAction(label, callback) {
        const item = super.addAction(label, callback);
        item.setOrnament(PopupMenu.Ornament.NONE);
        return item;
    }
}

export class WindowMenuManager {
    constructor() {
        this._manager = new PopupMenu.PopupMenuManager(Main.layoutManager.dummyCursor);

        this._sourceActor = new St.Widget({reactive: true, visible: false});
        this._sourceActor.connect('button-press-event', () => {
            this._manager.activeMenu.toggle();
        });
        Main.uiGroup.add_child(this._sourceActor);
    }

    showWindowMenuForWindow(window, type, rect) {
        if (!Main.sessionMode.hasWmMenus)
            return;

        if (type !== Meta.WindowMenuType.WM)
            throw new Error('Unsupported window menu type');
        let menu = new WindowMenu(window, this._sourceActor);

        this._manager.addMenu(menu);

        menu.connect('activate', () => {
            window.check_alive(global.get_current_time());
        });
        let destroyId = window.connect('unmanaged', () => {
            menu.close();
        });

        this._sourceActor.set_size(Math.max(1, rect.width), Math.max(1, rect.height));
        this._sourceActor.set_position(rect.x, rect.y);
        this._sourceActor.show();

        menu.open(BoxPointer.PopupAnimation.FADE);
        menu.actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
        menu.connect('open-state-changed', (menu_, isOpen) => {
            if (isOpen)
                return;

            this._sourceActor.hide();
            menu.destroy();
            window.disconnect(destroyId);
        });
    }
}
(uuay)osdMonitorLabeler.js-
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import St from 'gi://St';


import * as Main from './main.js';

const OsdMonitorLabel = GObject.registerClass(
class OsdMonitorLabel extends St.Widget {
    _init(monitor, label) {
        super._init({x_expand: true, y_expand: true});

        this._monitor = monitor;

        this._box = new St.BoxLayout({
            vertical: true,
        });
        this.add_child(this._box);

        this._label = new St.Label({
            style_class: 'osd-monitor-label',
            text: label,
        });
        this._box.add_child(this._label);

        Main.uiGroup.add_child(this);
        Main.uiGroup.set_child_above_sibling(this, null);
        this._position();

        Meta.disable_unredirect_for_display(global.display);
        this.connect('destroy', () => {
            Meta.enable_unredirect_for_display(global.display);
        });
    }

    _position() {
        let workArea = Main.layoutManager.getWorkAreaForMonitor(this._monitor);

        if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
            this._box.x = workArea.x + (workArea.width - this._box.width);
        else
            this._box.x = workArea.x;

        this._box.y = workArea.y;
    }
});

export class OsdMonitorLabeler {
    constructor() {
        this._monitorManager = global.backend.get_monitor_manager();
        this._client = null;
        this._clientWatchId = 0;
        this._osdLabels = [];
        this._monitorLabels = null;
        Main.layoutManager.connect('monitors-changed',
            this._reset.bind(this));
        this._reset();
    }

    _reset() {
        for (let i in this._osdLabels)
            this._osdLabels[i].destroy();
        this._osdLabels = [];
        this._monitorLabels = new Map();
        let monitors = Main.layoutManager.monitors;
        for (let i in monitors)
            this._monitorLabels.set(monitors[i].index, []);
    }

    _trackClient(client) {
        if (this._client)
            return this._client === client;

        this._client = client;
        this._clientWatchId = Gio.bus_watch_name(Gio.BusType.SESSION,
            client, 0, null,
            (c, name) => {
                this.hide(name);
            });
        return true;
    }

    _untrackClient(client) {
        if (!this._client || this._client !== client)
            return false;

        Gio.bus_unwatch_name(this._clientWatchId);
        this._clientWatchId = 0;
        this._client = null;
        return true;
    }

    show(client, params) {
        if (!this._trackClient(client))
            return;

        this._reset();

        for (let connector in params) {
            let monitor = this._monitorManager.get_monitor_for_connector(connector);
            if (monitor === -1)
                continue;
            this._monitorLabels.get(monitor).push(params[connector].deepUnpack());
        }

        for (let [monitor, labels] of this._monitorLabels.entries()) {
            labels.sort();
            this._osdLabels.push(new OsdMonitorLabel(monitor, labels.join(' ')));
        }
    }

    hide(client) {
        if (!this._untrackClient(client))
            return;

        this._reset();
    }
}
(uuay)messageList.jsBfimport Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import St from 'gi://St';

import * as Main from './main.js';
import * as MessageTray from './messageTray.js';

import * as Util from '../misc/util.js';
import {formatTimeSpan} from '../misc/dateUtils.js';

const MESSAGE_ANIMATION_TIME = 100;

const DEFAULT_EXPAND_LINES = 6;

/**
 * @param {string} text
 * @param {boolean} allowMarkup
 * @returns {string}
 */
export function _fixMarkup(text, allowMarkup) {
    if (allowMarkup) {
        // Support &amp;, &quot;, &apos;, &lt; and &gt;, escape all other
        // occurrences of '&'.
        let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&amp;');

        // Support <b>, <i>, and <u>, escape anything else
        // so it displays as raw markup.
        // Ref: https://developer.gnome.org/notification-spec/#markup
        _text = _text.replace(/<(?!\/?[biu]>)/g, '&lt;');

        try {
            Pango.parse_markup(_text, -1, '');
            return _text;
        } catch (e) {}
    }

    // !allowMarkup, or invalid markup
    return GLib.markup_escape_text(text, -1);
}

export const URLHighlighter = GObject.registerClass(
class URLHighlighter extends St.Label {
    _init(text = '', lineWrap, allowMarkup) {
        super._init({
            reactive: true,
            style_class: 'url-highlighter',
            x_expand: true,
            x_align: Clutter.ActorAlign.START,
        });
        this._linkColor = '#ccccff';
        this.connect('style-changed', () => {
            let [hasColor, color] = this.get_theme_node().lookup_color('link-color', false);
            if (hasColor) {
                let linkColor = color.to_string().substr(0, 7);
                if (linkColor !== this._linkColor) {
                    this._linkColor = linkColor;
                    this._highlightUrls();
                }
            }
        });
        this.clutter_text.line_wrap = lineWrap;
        this.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;

        this.setMarkup(text, allowMarkup);
    }

    vfunc_button_press_event(event) {
        // Don't try to URL highlight when invisible.
        // The MessageTray doesn't actually hide us, so
        // we need to check for paint opacities as well.
        if (!this.visible || this.get_paint_opacity() === 0)
            return Clutter.EVENT_PROPAGATE;

        // Keep Notification from seeing this and taking
        // a pointer grab, which would block our button-release-event
        // handler, if an URL is clicked
        return this._findUrlAtPos(event) !== -1;
    }

    vfunc_button_release_event(event) {
        if (!this.visible || this.get_paint_opacity() === 0)
            return Clutter.EVENT_PROPAGATE;

        const urlId = this._findUrlAtPos(event);
        if (urlId !== -1) {
            let url = this._urls[urlId].url;
            if (!url.includes(':'))
                url = `http://${url}`;

            Gio.app_info_launch_default_for_uri(
                url, global.create_app_launch_context(0, -1));
            return Clutter.EVENT_STOP;
        }
        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_motion_event(event) {
        if (!this.visible || this.get_paint_opacity() === 0)
            return Clutter.EVENT_PROPAGATE;

        const urlId = this._findUrlAtPos(event);
        if (urlId !== -1 && !this._cursorChanged) {
            global.display.set_cursor(Meta.Cursor.POINTING_HAND);
            this._cursorChanged = true;
        } else if (urlId === -1) {
            global.display.set_cursor(Meta.Cursor.DEFAULT);
            this._cursorChanged = false;
        }
        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_leave_event(event) {
        if (!this.visible || this.get_paint_opacity() === 0)
            return Clutter.EVENT_PROPAGATE;

        if (this._cursorChanged) {
            this._cursorChanged = false;
            global.display.set_cursor(Meta.Cursor.DEFAULT);
        }
        return super.vfunc_leave_event(event);
    }

    setMarkup(text, allowMarkup) {
        text = text ? _fixMarkup(text, allowMarkup) : '';
        this._text = text;

        this.clutter_text.set_markup(text);
        /* clutter_text.text contain text without markup */
        this._urls = Util.findUrls(this.clutter_text.text);
        this._highlightUrls();
    }

    _highlightUrls() {
        // text here contain markup
        let urls = Util.findUrls(this._text);
        let markup = '';
        let pos = 0;
        for (let i = 0; i < urls.length; i++) {
            let url = urls[i];
            let str = this._text.substr(pos, url.pos - pos);
            markup += `${str}<span foreground="${this._linkColor}"><u>${url.url}</u></span>`;
            pos = url.pos + url.url.length;
        }
        markup += this._text.substr(pos);
        this.clutter_text.set_markup(markup);
    }

    _findUrlAtPos(event) {
        let [x, y] = event.get_coords();
        [, x, y] = this.transform_stage_point(x, y);
        let findPos = -1;
        for (let i = 0; i < this.clutter_text.text.length; i++) {
            let [, px, py, lineHeight] = this.clutter_text.position_to_coords(i);
            if (py > y || py + lineHeight < y || x < px)
                continue;
            findPos = i;
        }
        if (findPos !== -1) {
            for (let i = 0; i < this._urls.length; i++) {
                if (findPos >= this._urls[i].pos &&
                    this._urls[i].pos + this._urls[i].url.length > findPos)
                    return i;
            }
        }
        return -1;
    }
});

const ScaleLayout = GObject.registerClass(
class ScaleLayout extends Clutter.BinLayout {
    _init(params) {
        this._container = null;
        super._init(params);
    }

    _connectContainer(container) {
        if (this._container === container)
            return;

        this._container?.disconnectObject(this);

        this._container = container;

        if (this._container) {
            this._container.connectObject(
                'notify::scale-x', () => this.layout_changed(),
                'notify::scale-y', () => this.layout_changed(), this);
        }
    }

    vfunc_get_preferred_width(container, forHeight) {
        this._connectContainer(container);

        let [min, nat] = super.vfunc_get_preferred_width(container, forHeight);
        return [
            Math.floor(min * container.scale_x),
            Math.floor(nat * container.scale_x),
        ];
    }

    vfunc_get_preferred_height(container, forWidth) {
        this._connectContainer(container);

        let [min, nat] = super.vfunc_get_preferred_height(container, forWidth);
        return [
            Math.floor(min * container.scale_y),
            Math.floor(nat * container.scale_y),
        ];
    }
});

const LabelExpanderLayout = GObject.registerClass({
    Properties: {
        'expansion': GObject.ParamSpec.double(
            'expansion', 'Expansion', 'Expansion',
            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
            0, 1, 0),
    },
}, class LabelExpanderLayout extends Clutter.BinLayout {
    constructor(params) {
        super(params);

        this._expansion = 0;
        this._expandLines = DEFAULT_EXPAND_LINES;
    }

    get expansion() {
        return this._expansion;
    }

    set expansion(v) {
        if (v === this._expansion)
            return;
        this._expansion = v;
        this.notify('expansion');

        this.layout_changed();
    }

    set expandLines(v) {
        if (v === this._expandLines)
            return;
        this._expandLines = v;
        if (this._expansion > 0)
            this.layout_changed();
    }

    vfunc_get_preferred_height(container, forWidth) {
        let [min, nat] = [0, 0];

        const [child] = container;

        if (child) {
            [min, nat] = child.get_preferred_height(-1);

            const [, nat2] = child.get_preferred_height(forWidth);
            const expHeight =
                Math.min(nat2, nat * this._expandLines);
            [min, nat] = [
                min + this._expansion * (expHeight - min),
                nat + this._expansion * (expHeight - nat),
            ];
        }

        return [min, nat];
    }
});

export const Source = GObject.registerClass({
    Properties: {
        'title': GObject.ParamSpec.string(
            'title', 'title', 'title',
            GObject.ParamFlags.READWRITE,
            null),
        'icon': GObject.ParamSpec.object(
            'icon', 'icon', 'icon',
            GObject.ParamFlags.READWRITE,
            Gio.Icon),
        'icon-name': GObject.ParamSpec.string(
            'icon-name', 'icon-name', 'icon-name',
            GObject.ParamFlags.READWRITE,
            null),
    },
}, class Source extends GObject.Object {
    get iconName() {
        if (this.gicon instanceof Gio.ThemedIcon)
            return this.gicon.iconName;
        else
            return null;
    }

    set iconName(iconName) {
        this.icon = new Gio.ThemedIcon({name: iconName});
    }
});

const TimeLabel = GObject.registerClass(
class TimeLabel extends St.Label {
    _init() {
        super._init({
            style_class: 'event-time',
            x_expand: true,
            y_expand: true,
            x_align: Clutter.ActorAlign.START,
            y_align: Clutter.ActorAlign.END,
            visible: false,
        });
    }

    get datetime() {
        return this._datetime;
    }

    set datetime(datetime) {
        if (this._datetime?.equal(datetime))
            return;

        this._datetime = datetime;

        this.visible = !!this._datetime;

        if (this.mapped)
            this._updateText();
    }

    _updateText() {
        if (this._datetime)
            this.text = formatTimeSpan(this._datetime);
    }

    vfunc_map() {
        this._updateText();

        super.vfunc_map();
    }
});

const MessageHeader = GObject.registerClass(
class MessageHeader extends St.BoxLayout {
    constructor(source) {
        super({
            style_class: 'message-header',
            x_expand: true,
        });

        const sourceIconEffect = new Clutter.DesaturateEffect();
        const sourceIcon = new St.Icon({
            style_class: 'message-source-icon',
            y_align: Clutter.ActorAlign.CENTER,
            fallback_icon_name: 'application-x-executable-symbolic',
        });
        sourceIcon.add_effect(sourceIconEffect);
        this.add_child(sourceIcon);

        sourceIcon.connect('style-changed', () => {
            const themeNode = sourceIcon.get_theme_node();
            sourceIconEffect.enabled = themeNode.get_icon_style() === St.IconStyle.SYMBOLIC;
        });

        const headerContent = new St.BoxLayout({
            style_class: 'message-header-content',
            y_align: Clutter.ActorAlign.CENTER,
            x_expand: true,
        });
        this.add_child(headerContent);

        this.expandButton = new St.Button({
            style_class: 'message-expand-button',
            icon_name: 'notification-expand-symbolic',
            y_align: Clutter.ActorAlign.CENTER,
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
        });
        this.add_child(this.expandButton);

        this.closeButton = new St.Button({
            style_class: 'message-close-button',
            icon_name: 'window-close-symbolic',
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this.closeButton);

        const sourceTitle = new St.Label({
            style_class: 'message-source-title',
            y_align: Clutter.ActorAlign.END,
        });
        headerContent.add_child(sourceTitle);

        source.bind_property_full('title',
            sourceTitle,
            'text',
            GObject.BindingFlags.SYNC_CREATE,
            // Translators: this is the string displayed in the header when a message
            // source doesn't have a name
            (bind, value) => [true, value === null || value === '' ? _('Unknown App') : value],
            null);
        source.bind_property('icon',
            sourceIcon,
            'gicon',
            GObject.BindingFlags.SYNC_CREATE);

        this.timeLabel = new TimeLabel();
        headerContent.add_child(this.timeLabel);
    }
});

export const Message = GObject.registerClass({
    Properties: {
        'title': GObject.ParamSpec.string(
            'title', 'title', 'title',
            GObject.ParamFlags.READWRITE,
            null),
        'body': GObject.ParamSpec.string(
            'body', 'body', 'body',
            GObject.ParamFlags.READWRITE,
            null),
        'use-body-markup': GObject.ParamSpec.boolean(
            'use-body-markup', 'use-body-markup', 'use-body-markup',
            GObject.ParamFlags.READWRITE,
            false),
        'icon': GObject.ParamSpec.object(
            'icon', 'icon', 'icon',
            GObject.ParamFlags.READWRITE,
            Gio.Icon),
        'datetime': GObject.ParamSpec.boxed(
            'datetime', 'datetime', 'datetime',
            GObject.ParamFlags.READWRITE,
            GLib.DateTime),
    },
    Signals: {
        'close': {},
        'expanded': {},
        'unexpanded': {},
    },
}, class Message extends St.Button {
    constructor(source) {
        super({
            style_class: 'message',
            accessible_role: Atk.Role.NOTIFICATION,
            can_focus: true,
            x_expand: true,
            y_expand: false,
        });

        this.expanded = false;
        this._useBodyMarkup = false;

        let vbox = new St.BoxLayout({
            vertical: true,
            x_expand: true,
        });
        this.set_child(vbox);

        this._header = new MessageHeader(source);
        vbox.add_child(this._header);

        const hbox = new St.BoxLayout({
            style_class: 'message-box',
        });
        vbox.add_child(hbox);

        this._actionBin = new St.Bin({
            layout_manager: new ScaleLayout(),
            visible: false,
        });
        vbox.add_child(this._actionBin);

        this._icon = new St.Icon({
            style_class: 'message-icon',
            y_expand: true,
            y_align: Clutter.ActorAlign.START,
            visible: false,
        });
        hbox.add_child(this._icon);

        const contentBox = new St.BoxLayout({
            style_class: 'message-content',
            vertical: true,
            x_expand: true,
        });
        hbox.add_child(contentBox);

        this._mediaControls = new St.BoxLayout();
        hbox.add_child(this._mediaControls);

        this.titleLabel = new St.Label({
            style_class: 'message-title',
            y_align: Clutter.ActorAlign.END,
        });
        contentBox.add_child(this.titleLabel);

        this._bodyLabel = new URLHighlighter('', true, this._useBodyMarkup);
        this._bodyLabel.add_style_class_name('message-body');
        this._bodyBin = new St.Bin({
            x_expand: true,
            layout_manager: new LabelExpanderLayout(),
            child: this._bodyLabel,
        });
        contentBox.add_child(this._bodyBin);

        this.connect('destroy', this._onDestroy.bind(this));

        this._header.closeButton.connect('clicked', this.close.bind(this));
        this._header.closeButton.visible = this.canClose();

        this._header.expandButton.connect('clicked', () => {
            if (this.expanded)
                this.unexpand(true);
            else
                this.expand(true);
        });
        this._bodyLabel.connect('notify::allocation', this._updateExpandButton.bind(this));
        this._updateExpandButton();
    }

    _updateExpandButton() {
        if (!this._bodyLabel.has_allocation())
            return;
        const layout = this._bodyLabel.clutter_text.get_layout();
        const canExpand = layout.is_ellipsized() || this.expanded || !!this._actionBin.child;
        // Use opacity to not trigger a relayout
        this._header.expandButton.opacity = canExpand ? 255 : 0;
    }

    close() {
        this.emit('close');
    }

    set icon(icon) {
        this._icon.gicon = icon;

        if (icon instanceof Gio.ThemedIcon)
            this._icon.add_style_class_name('message-themed-icon');
        else
            this._icon.remove_style_class_name('message-themed-icon');

        this._icon.visible = !!icon;
        this.notify('icon');
    }

    get icon() {
        return this._icon.gicon;
    }

    set datetime(datetime) {
        this._header.timeLabel.datetime = datetime;
        this.notify('datetime');
    }

    get datetime() {
        return this._header.timeLabel.datetime;
    }

    set title(text) {
        this._titleText = text;
        const title = text ? _fixMarkup(text.replace(/\n/g, ' '), false) : '';
        this.titleLabel.clutter_text.set_markup(title);
        this.notify('title');
    }

    get title() {
        return this._titleText;
    }

    set body(text) {
        this._bodyText = text;
        this._bodyLabel.setMarkup(text ? text.replace(/\n/g, ' ') : '',
            this._useBodyMarkup);
        this.notify('body');
    }

    get body() {
        return this._bodyText;
    }

    set useBodyMarkup(enable) {
        if (this._useBodyMarkup === enable)
            return;
        this._useBodyMarkup = enable;
        this.body = this._bodyText;
        this.notify('use-body-markup');
    }

    get useBodyMarkup() {
        return this._useBodyMarkup;
    }

    setActionArea(actor) {
        this._actionBin.child = actor;
        this._actionBin.visible = actor && this.expanded;
        this._updateExpandButton();
    }

    addMediaControl(iconName, callback) {
        const button = new St.Button({
            style_class: 'message-media-control',
            iconName,
        });
        button.connect('clicked', callback);
        this._mediaControls.add_child(button);
        return button;
    }

    expand(animate) {
        this.expanded = true;

        this._actionBin.visible = !!this._actionBin.child;

        const duration = animate ? MessageTray.ANIMATION_TIME : 0;
        this._bodyBin.ease_property('@layout.expansion', 1, {
            progress_mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            duration,
        });

        this._actionBin.scale_y = 0;
        this._actionBin.ease({
            scale_y: 1,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        this._header.expandButton.ease({
            rotation_angle_z: 180,
            duration,
        });

        this.emit('expanded');
    }

    unexpand(animate) {
        const duration = animate ? MessageTray.ANIMATION_TIME : 0;
        this._bodyBin.ease_property('@layout.expansion', 0, {
            progress_mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            duration,
        });

        this._actionBin.ease({
            scale_y: 0,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this._actionBin.hide();
                this.expanded = false;
            },
        });

        this._header.expandButton.ease({
            rotation_angle_z: 0,
            duration,
        });

        this.emit('unexpanded');
    }

    canClose() {
        return false;
    }

    _onDestroy() {
    }

    vfunc_key_press_event(event) {
        let keysym = event.get_key_symbol();

        if (keysym === Clutter.KEY_Delete ||
            keysym === Clutter.KEY_KP_Delete ||
            keysym === Clutter.KEY_BackSpace) {
            if (this.canClose()) {
                this.close();
                return Clutter.EVENT_STOP;
            }
        }
        return super.vfunc_key_press_event(event);
    }
});

export const MessageListSection = GObject.registerClass({
    Properties: {
        'can-clear': GObject.ParamSpec.boolean(
            'can-clear', 'can-clear', 'can-clear',
            GObject.ParamFlags.READABLE,
            false),
        'empty': GObject.ParamSpec.boolean(
            'empty', 'empty', 'empty',
            GObject.ParamFlags.READABLE,
            true),
    },
    Signals: {
        'can-clear-changed': {},
        'empty-changed': {},
        'message-focused': {param_types: [Message.$gtype]},
    },
}, class MessageListSection extends St.BoxLayout {
    _init() {
        super._init({
            style_class: 'message-list-section',
            clip_to_allocation: true,
            vertical: true,
            x_expand: true,
        });

        this._list = new St.BoxLayout({
            style_class: 'message-list-section-list',
            vertical: true,
        });
        this.add_child(this._list);

        this._list.connect('child-added', this._sync.bind(this));
        this._list.connect('child-removed', this._sync.bind(this));

        Main.sessionMode.connectObject(
            'updated', () => this._sync(), this);

        this._empty = true;
        this._canClear = false;
        this._sync();
    }

    get empty() {
        return this._empty;
    }

    get canClear() {
        return this._canClear;
    }

    get _messages() {
        return this._list.get_children().map(i => i.child);
    }

    _onKeyFocusIn(messageActor) {
        this.emit('message-focused', messageActor);
    }

    get allowed() {
        return true;
    }

    addMessage(message, animate) {
        this.addMessageAtIndex(message, -1, animate);
    }

    addMessageAtIndex(message, index, animate) {
        if (this._messages.includes(message))
            throw new Error('Message was already added previously');

        let listItem = new St.Bin({
            child: message,
            layout_manager: new ScaleLayout(),
            pivot_point: new Graphene.Point({x: .5, y: .5}),
        });
        listItem._connectionsIds = [];

        listItem._connectionsIds.push(message.connect('key-focus-in',
            this._onKeyFocusIn.bind(this)));
        listItem._connectionsIds.push(message.connect('close', () => {
            this.removeMessage(message, true);
        }));
        listItem._connectionsIds.push(message.connect('destroy', () => {
            listItem._connectionsIds.forEach(id => message.disconnect(id));
            listItem.destroy();
        }));

        this._list.insert_child_at_index(listItem, index);

        const duration = animate ? MESSAGE_ANIMATION_TIME : 0;
        listItem.set({scale_x: 0, scale_y: 0});
        listItem.ease({
            scale_x: 1,
            scale_y: 1,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    moveMessage(message, index, animate) {
        if (!this._messages.includes(message))
            throw new Error('Impossible to move untracked message');

        let listItem = message.get_parent();

        if (!animate) {
            this._list.set_child_at_index(listItem, index);
            return;
        }

        let onComplete = () => {
            this._list.set_child_at_index(listItem, index);
            listItem.ease({
                scale_x: 1,
                scale_y: 1,
                duration: MESSAGE_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        };
        listItem.ease({
            scale_x: 0,
            scale_y: 0,
            duration: MESSAGE_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete,
        });
    }

    removeMessage(message, animate) {
        const messages = this._messages;

        if (!messages.includes(message))
            throw new Error('Impossible to remove untracked message');

        let listItem = message.get_parent();
        listItem._connectionsIds.forEach(id => message.disconnect(id));

        let nextMessage = null;

        if (message.has_key_focus()) {
            const index = messages.indexOf(message);
            nextMessage =
                messages[index + 1] ||
                messages[index - 1] ||
                this._list;
        }

        const duration = animate ? MESSAGE_ANIMATION_TIME : 0;
        listItem.ease({
            scale_x: 0,
            scale_y: 0,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                listItem.destroy();
                nextMessage?.grab_key_focus();
            },
        });
    }

    clear() {
        let messages = this._messages.filter(msg => msg.canClose());

        // If there are few messages, letting them all zoom out looks OK
        if (messages.length < 2) {
            messages.forEach(message => {
                message.close();
            });
        } else {
            // Otherwise we slide them out one by one, and then zoom them
            // out "off-screen" in the end to smoothly shrink the parent
            let delay = MESSAGE_ANIMATION_TIME / Math.max(messages.length, 5);
            for (let i = 0; i < messages.length; i++) {
                let message = messages[i];
                message.get_parent().ease({
                    translation_x: this._list.width,
                    opacity: 0,
                    duration: MESSAGE_ANIMATION_TIME,
                    delay: i * delay,
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                    onComplete: () => message.close(),
                });
            }
        }
    }

    _shouldShow() {
        return !this.empty;
    }

    _sync() {
        let messages = this._messages;
        let empty = messages.length === 0;

        if (this._empty !== empty) {
            this._empty = empty;
            this.notify('empty');
        }

        let canClear = messages.some(m => m.canClose());
        if (this._canClear !== canClear) {
            this._canClear = canClear;
            this.notify('can-clear');
        }

        this.visible = this.allowed && this._shouldShow();
    }
});
(uuay)objectManager.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import * as Params from './params.js';
import * as Signals from './signals.js';

// Specified in the D-Bus specification here:
// http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager
const ObjectManagerIface = `
<node>
<interface name="org.freedesktop.DBus.ObjectManager">
  <method name="GetManagedObjects">
    <arg name="objects" type="a{oa{sa{sv}}}" direction="out"/>
  </method>
  <signal name="InterfacesAdded">
    <arg name="objectPath" type="o"/>
    <arg name="interfaces" type="a{sa{sv}}" />
  </signal>
  <signal name="InterfacesRemoved">
    <arg name="objectPath" type="o"/>
    <arg name="interfaces" type="as" />
  </signal>
</interface>
</node>`;

const ObjectManagerInfo = Gio.DBusInterfaceInfo.new_for_xml(ObjectManagerIface);

export class ObjectManager extends Signals.EventEmitter {
    constructor(params) {
        super();

        params = Params.parse(params, {
            connection: null,
            name: null,
            objectPath: null,
            knownInterfaces: null,
            cancellable: null,
            onLoaded: null,
        });

        this._connection = params.connection;
        this._serviceName = params.name;
        this._managerPath = params.objectPath;
        this._cancellable = params.cancellable;

        this._managerProxy = new Gio.DBusProxy({
            g_connection: this._connection,
            g_interface_name: ObjectManagerInfo.name,
            g_interface_info: ObjectManagerInfo,
            g_name: this._serviceName,
            g_object_path: this._managerPath,
            g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START,
        });

        this._interfaceInfos = {};
        this._objects = {};
        this._interfaces = {};
        this._onLoaded = params.onLoaded;

        if (params.knownInterfaces)
            this._registerInterfaces(params.knownInterfaces);

        this._initManagerProxy();
    }

    _completeLoad() {
        if (this._onLoaded)
            this._onLoaded();
    }

    async _addInterface(objectPath, interfaceName) {
        let info = this._interfaceInfos[interfaceName];

        if (!info)
            return;

        const proxy = new Gio.DBusProxy({
            g_connection: this._connection,
            g_name: this._serviceName,
            g_object_path: objectPath,
            g_interface_name: interfaceName,
            g_interface_info: info,
            g_flags: Gio.DBusProxyFlags.DO_NOT_AUTO_START,
        });

        try {
            await proxy.init_async(GLib.PRIORITY_DEFAULT, this._cancellable);
        } catch (e) {
            logError(e, `could not initialize proxy for interface ${interfaceName}`);
            return;
        }

        let isNewObject;
        if (!this._objects[objectPath]) {
            this._objects[objectPath] = {};
            isNewObject = true;
        } else {
            isNewObject = false;
        }

        this._objects[objectPath][interfaceName] = proxy;

        if (!this._interfaces[interfaceName])
            this._interfaces[interfaceName] = [];

        this._interfaces[interfaceName].push(proxy);

        if (isNewObject)
            this.emit('object-added', objectPath);

        this.emit('interface-added', interfaceName, proxy);
    }

    _removeInterface(objectPath, interfaceName) {
        if (!this._objects[objectPath])
            return;

        let proxy = this._objects[objectPath][interfaceName];

        if (this._interfaces[interfaceName]) {
            let index = this._interfaces[interfaceName].indexOf(proxy);

            if (index >= 0)
                this._interfaces[interfaceName].splice(index, 1);

            if (this._interfaces[interfaceName].length === 0)
                delete this._interfaces[interfaceName];
        }

        this.emit('interface-removed', interfaceName, proxy);

        delete this._objects[objectPath][interfaceName];

        if (Object.keys(this._objects[objectPath]).length === 0) {
            delete this._objects[objectPath];
            this.emit('object-removed', objectPath);
        }
    }

    async _initManagerProxy() {
        try {
            await this._managerProxy.init_async(
                GLib.PRIORITY_DEFAULT, this._cancellable);
        } catch (e) {
            logError(e, `could not initialize object manager for object ${this._serviceName}`);

            this._completeLoad();
            return;
        }

        this._managerProxy.connectSignal('InterfacesAdded',
            (objectManager, sender, [objectPath, interfaces]) => {
                let interfaceNames = Object.keys(interfaces);
                for (let i = 0; i < interfaceNames.length; i++)
                    this._addInterface(objectPath, interfaceNames[i]);
            });
        this._managerProxy.connectSignal('InterfacesRemoved',
            (objectManager, sender, [objectPath, interfaceNames]) => {
                for (let i = 0; i < interfaceNames.length; i++)
                    this._removeInterface(objectPath, interfaceNames[i]);
            });

        if (Object.keys(this._interfaceInfos).length === 0) {
            this._completeLoad();
            return;
        }

        this._managerProxy.connect('notify::g-name-owner', () => {
            if (this._managerProxy.g_name_owner)
                this._onNameAppeared();
            else
                this._onNameVanished();
        });

        if (this._managerProxy.g_name_owner)
            this._onNameAppeared();
    }

    async _onNameAppeared() {
        try {
            const [objects] = await this._managerProxy.GetManagedObjectsAsync();

            if (!objects) {
                this._completeLoad();
                return;
            }

            const objectPaths = Object.keys(objects);
            await Promise.allSettled(objectPaths.flatMap(objectPath => {
                const object = objects[objectPath];
                const interfaceNames = Object.getOwnPropertyNames(object);
                return interfaceNames.map(
                    ifaceName => this._addInterface(objectPath, ifaceName));
            }));
        } catch (error) {
            logError(error, `could not get remote objects for service ${this._serviceName} path ${this._managerPath}`);
        } finally {
            this._completeLoad();
        }
    }

    _onNameVanished() {
        let objectPaths = Object.keys(this._objects);
        for (let i = 0; i < objectPaths.length; i++) {
            let objectPath = objectPaths[i];
            let object = this._objects[objectPath];

            let interfaceNames = Object.keys(object);
            for (let j = 0; j < interfaceNames.length; j++) {
                let interfaceName = interfaceNames[j];

                if (object[interfaceName])
                    this._removeInterface(objectPath, interfaceName);
            }
        }
    }

    _registerInterfaces(interfaces) {
        for (let i = 0; i < interfaces.length; i++) {
            let info = Gio.DBusInterfaceInfo.new_for_xml(interfaces[i]);
            this._interfaceInfos[info.name] = info;
        }
    }

    getProxy(objectPath, interfaceName) {
        let object = this._objects[objectPath];

        if (!object)
            return null;

        return object[interfaceName];
    }

    getProxiesForInterface(interfaceName) {
        let proxyList = this._interfaces[interfaceName];

        if (!proxyList)
            return [];

        return proxyList;
    }

    getAllProxies() {
        let proxies = [];

        let objectPaths = Object.keys(this._objects);
        for (let i = 0; i < objectPaths.length; i++) {
            let object = this._objects[objectPaths];

            let interfaceNames = Object.keys(object);
            for (let j = 0; j < interfaceNames.length; j++) {
                let interfaceName = interfaceNames[j];
                if (object[interfaceName])
                    proxies.push(object(interfaceName));
            }
        }

        return proxies;
    }
}
(uuay)boxpointer.js{Y// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import St from 'gi://St';

import * as Main from './main.js';

export const PopupAnimation = {
    NONE:  0,
    SLIDE: 1 << 0,
    FADE:  1 << 1,
    FULL:  ~0,
};

const POPUP_ANIMATION_TIME = 150;

/**
 * BoxPointer:
 *
 * An actor which displays a triangle "arrow" pointing to a given
 * side.  The .bin property is a container in which content can be
 * placed.  The arrow position may be controlled via
 * setArrowOrigin(). The arrow side might be temporarily flipped
 * depending on the box size and source position to keep the box
 * totally inside the monitor workarea if possible.
 *
 */
export const BoxPointer = GObject.registerClass({
    Signals: {'arrow-side-changed': {}},
}, class BoxPointer extends St.Widget {
    /**
     * @param {*} arrowSide side to draw the arrow on
     * @param {*} binProperties Properties to set on contained bin
     */
    _init(arrowSide, binProperties) {
        super._init();

        this.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);

        this._arrowSide = arrowSide;
        this._userArrowSide = arrowSide;
        this._arrowOrigin = 0;
        this._arrowActor = null;
        this.bin = new St.Bin(binProperties);
        this.add_child(this.bin);
        this._border = new St.DrawingArea();
        this._border.connect('repaint', this._drawBorder.bind(this));
        this.add_child(this._border);
        this.set_child_above_sibling(this.bin, this._border);
        this._sourceAlignment = 0.5;
        this._muteKeys = true;
        this._muteInput = true;

        this.connect('notify::visible', () => {
            if (this.visible)
                Meta.disable_unredirect_for_display(global.display);
            else
                Meta.enable_unredirect_for_display(global.display);
        });
    }

    vfunc_captured_event(event) {
        if (event.type() === Clutter.EventType.ENTER ||
            event.type() === Clutter.EventType.LEAVE)
            return Clutter.EVENT_PROPAGATE;

        let mute = event.type() === Clutter.EventType.KEY_PRESS ||
            event.type() === Clutter.EventType.KEY_RELEASE
            ? this._muteKeys : this._muteInput;

        if (mute)
            return Clutter.EVENT_STOP;

        return Clutter.EVENT_PROPAGATE;
    }

    get arrowSide() {
        return this._arrowSide;
    }

    open(animate, onComplete) {
        let themeNode = this.get_theme_node();
        let rise = themeNode.get_length('-arrow-rise');
        let animationTime = animate & PopupAnimation.FULL ? POPUP_ANIMATION_TIME : 0;

        if (animate & PopupAnimation.FADE)
            this.opacity = 0;
        else
            this.opacity = 255;

        this._muteKeys = false;
        this.show();

        if (animate & PopupAnimation.SLIDE) {
            switch (this._arrowSide) {
            case St.Side.TOP:
                this.translation_y = -rise;
                break;
            case St.Side.BOTTOM:
                this.translation_y = rise;
                break;
            case St.Side.LEFT:
                this.translation_x = -rise;
                break;
            case St.Side.RIGHT:
                this.translation_x = rise;
                break;
            }
        }

        this.ease({
            opacity: 255,
            translation_x: 0,
            translation_y: 0,
            duration: animationTime,
            mode: Clutter.AnimationMode.LINEAR,
            onComplete: () => {
                this._muteInput = false;
                if (onComplete)
                    onComplete();
            },
        });
    }

    close(animate, onComplete) {
        if (!this.visible)
            return;

        let translationX = 0;
        let translationY = 0;
        let themeNode = this.get_theme_node();
        let rise = themeNode.get_length('-arrow-rise');
        let fade = animate & PopupAnimation.FADE;
        let animationTime = animate & PopupAnimation.FULL ? POPUP_ANIMATION_TIME : 0;

        if (animate & PopupAnimation.SLIDE) {
            switch (this._arrowSide) {
            case St.Side.TOP:
                translationY = rise;
                break;
            case St.Side.BOTTOM:
                translationY = -rise;
                break;
            case St.Side.LEFT:
                translationX = rise;
                break;
            case St.Side.RIGHT:
                translationX = -rise;
                break;
            }
        }

        this._muteInput = true;
        this._muteKeys = true;

        this.remove_all_transitions();
        this.ease({
            opacity: fade ? 0 : 255,
            translation_x: translationX,
            translation_y: translationY,
            duration: animationTime,
            mode: Clutter.AnimationMode.LINEAR,
            onComplete: () => {
                this.hide();
                this.opacity = 0;
                this.translation_x = 0;
                this.translation_y = 0;
                if (onComplete)
                    onComplete();
            },
        });
    }

    _adjustAllocationForArrow(isWidth, minSize, natSize) {
        let themeNode = this.get_theme_node();
        let borderWidth = themeNode.get_length('-arrow-border-width');
        minSize += borderWidth * 2;
        natSize += borderWidth * 2;
        if ((!isWidth && (this._arrowSide === St.Side.TOP || this._arrowSide === St.Side.BOTTOM)) ||
            (isWidth && (this._arrowSide === St.Side.LEFT || this._arrowSide === St.Side.RIGHT))) {
            let rise = themeNode.get_length('-arrow-rise');
            minSize += rise;
            natSize += rise;
        }

        return [minSize, natSize];
    }

    vfunc_get_preferred_width(forHeight) {
        let themeNode = this.get_theme_node();
        forHeight = themeNode.adjust_for_height(forHeight);

        let width = this.bin.get_preferred_width(forHeight);
        width = this._adjustAllocationForArrow(true, ...width);

        return themeNode.adjust_preferred_width(...width);
    }

    vfunc_get_preferred_height(forWidth) {
        let themeNode = this.get_theme_node();
        let borderWidth = themeNode.get_length('-arrow-border-width');
        forWidth = themeNode.adjust_for_width(forWidth);

        let height = this.bin.get_preferred_height(forWidth - 2 * borderWidth);
        height = this._adjustAllocationForArrow(false, ...height);

        return themeNode.adjust_preferred_height(...height);
    }

    vfunc_allocate(box) {
        if (this._sourceActor && this._sourceActor.mapped) {
            this._reposition(box);
            this._updateFlip(box);
        }

        this.set_allocation(box);

        let themeNode = this.get_theme_node();
        let borderWidth = themeNode.get_length('-arrow-border-width');
        let rise = themeNode.get_length('-arrow-rise');
        let childBox = new Clutter.ActorBox();
        let [availWidth, availHeight] = themeNode.get_content_box(box).get_size();

        childBox.x1 = 0;
        childBox.y1 = 0;
        childBox.x2 = availWidth;
        childBox.y2 = availHeight;
        this._border.allocate(childBox);

        childBox.x1 = borderWidth;
        childBox.y1 = borderWidth;
        childBox.x2 = availWidth - borderWidth;
        childBox.y2 = availHeight - borderWidth;
        switch (this._arrowSide) {
        case St.Side.TOP:
            childBox.y1 += rise;
            break;
        case St.Side.BOTTOM:
            childBox.y2 -= rise;
            break;
        case St.Side.LEFT:
            childBox.x1 += rise;
            break;
        case St.Side.RIGHT:
            childBox.x2 -= rise;
            break;
        }
        this.bin.allocate(childBox);
    }

    _drawBorder(area) {
        let themeNode = this.get_theme_node();

        if (this._arrowActor) {
            let [sourceX, sourceY] = this._arrowActor.get_transformed_position();
            let [sourceWidth, sourceHeight] = this._arrowActor.get_transformed_size();
            let [absX, absY] = this.get_transformed_position();

            if (this._arrowSide === St.Side.TOP ||
                this._arrowSide === St.Side.BOTTOM)
                this._arrowOrigin = sourceX - absX + sourceWidth / 2;
            else
                this._arrowOrigin = sourceY - absY + sourceHeight / 2;
        }

        let borderWidth = themeNode.get_length('-arrow-border-width');
        let base = themeNode.get_length('-arrow-base');
        let rise = themeNode.get_length('-arrow-rise');
        let borderRadius = themeNode.get_length('-arrow-border-radius');

        let halfBorder = borderWidth / 2;
        let halfBase = Math.floor(base / 2);

        let [width, height] = area.get_surface_size();
        let [boxWidth, boxHeight] = [width, height];
        if (this._arrowSide === St.Side.TOP || this._arrowSide === St.Side.BOTTOM)
            boxHeight -= rise;
        else
            boxWidth -= rise;

        let cr = area.get_context();

        // Translate so that box goes from 0,0 to boxWidth,boxHeight,
        // with the arrow poking out of that
        if (this._arrowSide === St.Side.TOP)
            cr.translate(0, rise);
        else if (this._arrowSide === St.Side.LEFT)
            cr.translate(rise, 0);

        let [x1, y1] = [halfBorder, halfBorder];
        let [x2, y2] = [boxWidth - halfBorder, boxHeight - halfBorder];

        let skipTopLeft = false;
        let skipTopRight = false;
        let skipBottomLeft = false;
        let skipBottomRight = false;

        if (rise) {
            switch (this._arrowSide) {
            case St.Side.TOP:
                if (this._arrowOrigin === x1)
                    skipTopLeft = true;
                else if (this._arrowOrigin === x2)
                    skipTopRight = true;
                break;

            case St.Side.RIGHT:
                if (this._arrowOrigin === y1)
                    skipTopRight = true;
                else if (this._arrowOrigin === y2)
                    skipBottomRight = true;
                break;

            case St.Side.BOTTOM:
                if (this._arrowOrigin === x1)
                    skipBottomLeft = true;
                else if (this._arrowOrigin === x2)
                    skipBottomRight = true;
                break;

            case St.Side.LEFT:
                if (this._arrowOrigin === y1)
                    skipTopLeft = true;
                else if (this._arrowOrigin === y2)
                    skipBottomLeft = true;
                break;
            }
        }

        cr.moveTo(x1 + borderRadius, y1);
        if (this._arrowSide === St.Side.TOP && rise) {
            if (skipTopLeft) {
                cr.moveTo(x1, y2 - borderRadius);
                cr.lineTo(x1, y1 - rise);
                cr.lineTo(x1 + halfBase, y1);
            } else if (skipTopRight) {
                cr.lineTo(x2 - halfBase, y1);
                cr.lineTo(x2, y1 - rise);
                cr.lineTo(x2, y1 + borderRadius);
            } else {
                cr.lineTo(this._arrowOrigin - halfBase, y1);
                cr.lineTo(this._arrowOrigin, y1 - rise);
                cr.lineTo(this._arrowOrigin + halfBase, y1);
            }
        }

        if (!skipTopRight) {
            cr.lineTo(x2 - borderRadius, y1);
            cr.arc(
                x2 - borderRadius, y1 + borderRadius, borderRadius,
                3 * Math.PI / 2, Math.PI * 2);
        }

        if (this._arrowSide === St.Side.RIGHT && rise) {
            if (skipTopRight) {
                cr.lineTo(x2 + rise, y1);
                cr.lineTo(x2 + rise, y1 + halfBase);
            } else if (skipBottomRight) {
                cr.lineTo(x2, y2 - halfBase);
                cr.lineTo(x2 + rise, y2);
                cr.lineTo(x2 - borderRadius, y2);
            } else {
                cr.lineTo(x2, this._arrowOrigin - halfBase);
                cr.lineTo(x2 + rise, this._arrowOrigin);
                cr.lineTo(x2, this._arrowOrigin + halfBase);
            }
        }

        if (!skipBottomRight) {
            cr.lineTo(x2, y2 - borderRadius);
            cr.arc(
                x2 - borderRadius, y2 - borderRadius, borderRadius,
                0, Math.PI / 2);
        }

        if (this._arrowSide === St.Side.BOTTOM && rise) {
            if (skipBottomLeft) {
                cr.lineTo(x1 + halfBase, y2);
                cr.lineTo(x1, y2 + rise);
                cr.lineTo(x1, y2 - borderRadius);
            } else if (skipBottomRight) {
                cr.lineTo(x2, y2 + rise);
                cr.lineTo(x2 - halfBase, y2);
            } else {
                cr.lineTo(this._arrowOrigin + halfBase, y2);
                cr.lineTo(this._arrowOrigin, y2 + rise);
                cr.lineTo(this._arrowOrigin - halfBase, y2);
            }
        }

        if (!skipBottomLeft) {
            cr.lineTo(x1 + borderRadius, y2);
            cr.arc(
                x1 + borderRadius, y2 - borderRadius, borderRadius,
                Math.PI / 2, Math.PI);
        }

        if (this._arrowSide === St.Side.LEFT && rise) {
            if (skipTopLeft) {
                cr.lineTo(x1, y1 + halfBase);
                cr.lineTo(x1 - rise, y1);
                cr.lineTo(x1 + borderRadius, y1);
            } else if (skipBottomLeft) {
                cr.lineTo(x1 - rise, y2);
                cr.lineTo(x1 - rise, y2 - halfBase);
            } else {
                cr.lineTo(x1, this._arrowOrigin + halfBase);
                cr.lineTo(x1 - rise, this._arrowOrigin);
                cr.lineTo(x1, this._arrowOrigin - halfBase);
            }
        }

        if (!skipTopLeft) {
            cr.lineTo(x1, y1 + borderRadius);
            cr.arc(
                x1 + borderRadius, y1 + borderRadius, borderRadius,
                Math.PI, 3 * Math.PI / 2);
        }

        const [hasColor, bgColor] =
            themeNode.lookup_color('-arrow-background-color', false);
        if (hasColor) {
            cr.setSourceColor(bgColor);
            cr.fillPreserve();
        }

        if (borderWidth > 0) {
            let borderColor = themeNode.get_color('-arrow-border-color');
            cr.setSourceColor(borderColor);
            cr.setLineWidth(borderWidth);
            cr.stroke();
        }

        cr.$dispose();
    }

    setPosition(sourceActor, alignment) {
        if (!this._sourceActor || sourceActor !== this._sourceActor) {
            this._sourceActor?.disconnectObject(this);

            this._sourceActor = sourceActor;

            this._sourceActor?.connectObject('destroy',
                () => (this._sourceActor = null), this);
        }

        this._arrowAlignment = Math.clamp(alignment, 0.0, 1.0);

        this.queue_relayout();
    }

    setSourceAlignment(alignment) {
        this._sourceAlignment = Math.clamp(alignment, 0.0, 1.0);

        if (!this._sourceActor)
            return;

        this.setPosition(this._sourceActor, this._arrowAlignment);
    }

    _reposition(allocationBox) {
        let sourceActor = this._sourceActor;
        let alignment = this._arrowAlignment;
        let monitorIndex = Main.layoutManager.findIndexForActor(sourceActor);

        this._sourceExtents = sourceActor.get_transformed_extents();
        this._workArea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);

        // Position correctly relative to the sourceActor
        const sourceAllocation = sourceActor.get_allocation_box();
        const sourceContentBox = sourceActor instanceof St.Widget
            ? sourceActor.get_theme_node().get_content_box(sourceAllocation)
            : new Clutter.ActorBox({
                x2: sourceAllocation.get_width(),
                y2: sourceAllocation.get_height(),
            });
        let sourceTopLeft = this._sourceExtents.get_top_left();
        let sourceBottomRight = this._sourceExtents.get_bottom_right();
        let sourceCenterX = sourceTopLeft.x + sourceContentBox.x1 + (sourceContentBox.x2 - sourceContentBox.x1) * this._sourceAlignment;
        let sourceCenterY = sourceTopLeft.y + sourceContentBox.y1 + (sourceContentBox.y2 - sourceContentBox.y1) * this._sourceAlignment;
        let [, , natWidth, natHeight] = this.get_preferred_size();

        // We also want to keep it onscreen, and separated from the
        // edge by the same distance as the main part of the box is
        // separated from its sourceActor
        let workarea = this._workArea;
        let themeNode = this.get_theme_node();
        let borderWidth = themeNode.get_length('-arrow-border-width');
        let arrowBase = themeNode.get_length('-arrow-base');
        let borderRadius = themeNode.get_length('-arrow-border-radius');
        let margin = 4 * borderRadius + borderWidth + arrowBase;

        let gap = themeNode.get_length('-boxpointer-gap');
        let padding = themeNode.get_length('-arrow-rise');

        let resX, resY;

        switch (this._arrowSide) {
        case St.Side.TOP:
            resY = sourceBottomRight.y + gap;
            break;
        case St.Side.BOTTOM:
            resY = sourceTopLeft.y - natHeight - gap;
            break;
        case St.Side.LEFT:
            resX = sourceBottomRight.x + gap;
            break;
        case St.Side.RIGHT:
            resX = sourceTopLeft.x - natWidth - gap;
            break;
        }

        // Now align and position the pointing axis, making sure it fits on
        // screen. If the arrowOrigin is so close to the edge that the arrow
        // will not be isosceles, we try to compensate as follows:
        //   - We skip the rounded corner and settle for a right angled arrow
        //     as shown below. See _drawBorder for further details.
        //     |\_____
        //     |
        //     |
        //   - If the arrow was going to be acute angled, we move the position
        //     of the box to maintain the arrow's accuracy.

        let arrowOrigin;
        let halfBase = Math.floor(arrowBase / 2);
        let halfBorder = borderWidth / 2;
        let halfMargin = margin / 2;
        let [x1, y1] = [halfBorder, halfBorder];
        let [x2, y2] = [natWidth - halfBorder, natHeight - halfBorder];

        switch (this._arrowSide) {
        case St.Side.TOP:
        case St.Side.BOTTOM:
            if (this.text_direction === Clutter.TextDirection.RTL)
                alignment = 1.0 - alignment;

            resX = sourceCenterX - (halfMargin + (natWidth - margin) * alignment);

            resX = Math.max(resX, workarea.x + padding);
            resX = Math.min(resX, workarea.x + workarea.width - (padding + natWidth));

            arrowOrigin = sourceCenterX - resX;
            if (arrowOrigin <= (x1 + (borderRadius + halfBase))) {
                if (arrowOrigin > x1)
                    resX += arrowOrigin - x1;
                arrowOrigin = x1;
            } else if (arrowOrigin >= (x2 - (borderRadius + halfBase))) {
                if (arrowOrigin < x2)
                    resX -= x2 - arrowOrigin;
                arrowOrigin = x2;
            }
            break;

        case St.Side.LEFT:
        case St.Side.RIGHT:
            resY = sourceCenterY - (halfMargin + (natHeight - margin) * alignment);

            resY = Math.max(resY, workarea.y + padding);
            resY = Math.min(resY, workarea.y + workarea.height - (padding + natHeight));

            arrowOrigin = sourceCenterY - resY;
            if (arrowOrigin <= (y1 + (borderRadius + halfBase))) {
                if (arrowOrigin > y1)
                    resY += arrowOrigin - y1;
                arrowOrigin = y1;
            } else if (arrowOrigin >= (y2 - (borderRadius + halfBase))) {
                if (arrowOrigin < y2)
                    resY -= y2 - arrowOrigin;
                arrowOrigin = y2;
            }
            break;
        }

        this.setArrowOrigin(arrowOrigin);

        let parent = this.get_parent();
        let success, x, y;
        while (!success) {
            [success, x, y] = parent.transform_stage_point(resX, resY);
            parent = parent.get_parent();
        }

        // Actually set the position
        allocationBox.set_origin(Math.floor(x), Math.floor(y));
    }

    // @origin: Coordinate specifying middle of the arrow, along
    // the Y axis for St.Side.LEFT, St.Side.RIGHT from the top and X axis from
    // the left for St.Side.TOP and St.Side.BOTTOM.
    setArrowOrigin(origin) {
        if (this._arrowOrigin !== origin) {
            this._arrowOrigin = origin;
            this._border.queue_repaint();
        }
    }

    // @actor: an actor relative to which the arrow is positioned.
    // Differently from setPosition, this will not move the boxpointer itself,
    // on the arrow
    setArrowActor(actor) {
        if (this._arrowActor !== actor) {
            this._arrowActor = actor;
            this._border.queue_repaint();
        }
    }

    _calculateArrowSide(arrowSide) {
        let sourceTopLeft = this._sourceExtents.get_top_left();
        let sourceBottomRight = this._sourceExtents.get_bottom_right();
        let [, , boxWidth, boxHeight] = this.get_preferred_size();
        let workarea = this._workArea;

        switch (arrowSide) {
        case St.Side.TOP:
            if (sourceBottomRight.y + boxHeight > workarea.y + workarea.height &&
                boxHeight < sourceTopLeft.y - workarea.y)
                return St.Side.BOTTOM;
            break;
        case St.Side.BOTTOM:
            if (sourceTopLeft.y - boxHeight < workarea.y &&
                boxHeight < workarea.y + workarea.height - sourceBottomRight.y)
                return St.Side.TOP;
            break;
        case St.Side.LEFT:
            if (sourceBottomRight.x + boxWidth > workarea.x + workarea.width &&
                boxWidth < sourceTopLeft.x - workarea.x)
                return St.Side.RIGHT;
            break;
        case St.Side.RIGHT:
            if (sourceTopLeft.x - boxWidth < workarea.x &&
                boxWidth < workarea.x + workarea.width - sourceBottomRight.x)
                return St.Side.LEFT;
            break;
        }

        return arrowSide;
    }

    _updateFlip(allocationBox) {
        let arrowSide = this._calculateArrowSide(this._userArrowSide);
        if (this._arrowSide !== arrowSide) {
            this._arrowSide = arrowSide;
            this._reposition(allocationBox);

            this.emit('arrow-side-changed');
        }
    }

    updateArrowSide(side) {
        this._arrowSide = side;
        this._border.queue_repaint();

        this.emit('arrow-side-changed');
    }

    getPadding(side) {
        return this.bin.get_theme_node().get_padding(side);
    }

    getArrowHeight() {
        return this.get_theme_node().get_length('-arrow-rise');
    }
});
(uuay)unifiedMechanism.jsq
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GObject from 'gi://GObject';
import * as Signals from '../misc/signals.js';
import {
    PASSWORD_ROLE_NAME,
    WEB_LOGIN_ROLE_NAME
} from './const.js';

export const MECHANISM_PROTOCOL = 'auth-mechanisms';

const AUTH_SELECTION_COMPLETION_STATUS = 'Ok';

export const SUPPORTED_ROLES = [
    PASSWORD_ROLE_NAME,
    WEB_LOGIN_ROLE_NAME,
];

export class UnifiedAuthService extends Signals.EventEmitter {
    // FIXME USE GObject so we have proper signal definitions
    // signals:
    // : 'mechanisms-changed'
    // : 'proto-request-handled'
    // : 'clear-message-queue'
    // : 'queue-message'
    // : 'reset'
    // : 'cancel'

    get protocolName() {
        throw new GObject.NotImplementedError(
            `protocolName in ${this.constructor.name}`);
    }

    getSupportedRoles() {
        throw new GObject.NotImplementedError(
            `getSupportedRoles in ${this.constructor.name}`);
    }

    getMechanisms() {
        throw new GObject.NotImplementedError(
            `getMechanisms in ${this.constructor.name}`);
    }

    handleProtocolRequest(_protocol, _version, _json) {
        throw new GObject.NotImplementedError(
            `getMechanisms in ${this.constructor.name}`);
    }

    handleMechanism(_mechanism) {
        return false;
    }

    handleAuthSelectionResponse(_mechanism, _role, _response) {
        return false;
    }

    sendProtocolResponse(reply) {
        this.emit('protocol-request-handled', reply);
    }

    getProtocolResponse(_mechanism, _role, _response) {
        throw new GObject.NotImplementedError(
            `getMechanisms in ${this.constructor.name}`);
    }

    setForegroundMechanism(_mechanism) {
        return false;
    }

    cancelRequested() {
        return false;
    }

    cancel() {}

    reset() {}
}

export class UnifiedMechanismProtocolHandler extends  UnifiedAuthService/* Signals.EventEmitter */ {
    get protocolName() {
        return MECHANISM_PROTOCOL;
    }

    getSupportedRoles() {
        return SUPPORTED_ROLES;
    }

    handleProtocolRequest(_protocol, _version, json) {
        let requestObject;

        try {
            requestObject = JSON.parse(json);
        } catch (e) {
            logError(e);
            return;
        }

        const authSelection = requestObject['auth-selection'];

        if (authSelection)
            this._handleAuthSelection(authSelection);
    }

    _handleAuthSelection(authSelection) {
        const mechanisms = authSelection.mechanisms;
        const priorityList = authSelection.priority;
        const mechanismsList = [];

        if (!mechanisms)
            return;

        const ids = Object.keys(mechanisms);
        ids.sort((a, b) => {
            const priorityA = priorityList.indexOf(a);
            const priorityB = priorityList.indexOf(b);

            if (priorityA !== -1 && priorityB !== -1)
                return priorityA - priorityB;

            if (priorityA !== -1)
                return -1;

            if (priorityB !== -1)
                return 1;

            return 0;
        });

        this.emit('mechanisms-changed', mechanisms);
    }

    getProtocolResponse(_mechanism, role, response) {
        return {
            'auth-selection': {
                status: AUTH_SELECTION_COMPLETION_STATUS,
                mechanismId: response,
            },
        };
    }
}
(uuay)parentalControlsManager.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
//
// Copyright (C) 2018, 2019, 2020 Endless Mobile, Inc.
//
// This is a GNOME Shell component to wrap the interactions over
// D-Bus with the malcontent library.
//
// Licensed under the GNU General Public License Version 2
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';

// We require libmalcontent ≥ 0.6.0
const HAVE_MALCONTENT = imports.package.checkSymbol(
    'Malcontent', '0', 'ManagerGetValueFlags');

let Malcontent = null;
if (HAVE_MALCONTENT) {
    ({default: Malcontent} = await import('gi://Malcontent?version=0'));
    Gio._promisify(Malcontent.Manager.prototype, 'get_app_filter_async');
}

let _singleton = null;

/**
 * @returns {ParentalControlsManager}
 */
export function getDefault() {
    if (_singleton === null)
        _singleton = new ParentalControlsManager();

    return _singleton;
}

// A manager class which provides cached access to the constructing user’s
// parental controls settings. It’s possible for the user’s parental controls
// to change at runtime if the Parental Controls application is used by an
// administrator from within the user’s session.
const ParentalControlsManager = GObject.registerClass({
    Signals: {
        'app-filter-changed': {},
    },
}, class ParentalControlsManager extends GObject.Object {
    _init() {
        super._init();

        this._initialized = false;
        this._disabled = false;
        this._appFilter = null;

        this._initializeManager();
    }

    async _initializeManager() {
        if (!HAVE_MALCONTENT) {
            console.debug('Skipping parental controls support, malcontent not found');
            this._initialized = true;
            this.emit('app-filter-changed');
            return;
        }

        try {
            const connection = await Gio.DBus.get(Gio.BusType.SYSTEM, null);
            this._manager = new Malcontent.Manager({connection});
            this._appFilter = await this._getAppFilter();
        } catch (e) {
            logError(e, 'Failed to get parental controls settings');
            return;
        }

        this._manager.connect('app-filter-changed', this._onAppFilterChanged.bind(this));

        // Signal initialisation is complete.
        this._initialized = true;
        this.emit('app-filter-changed');
    }

    async _getAppFilter() {
        let appFilter = null;

        try {
            appFilter = await this._manager.get_app_filter_async(
                Shell.util_get_uid(),
                Malcontent.ManagerGetValueFlags.NONE,
                null);
        } catch (e) {
            if (!e.matches(Malcontent.ManagerError, Malcontent.ManagerError.DISABLED))
                throw e;

            console.debug('Parental controls globally disabled');
            this._disabled = true;
        }

        return appFilter;
    }

    async _onAppFilterChanged(manager, uid) {
        // Emit 'changed' signal only if app-filter is changed for currently logged-in user.
        let currentUid = Shell.util_get_uid();
        if (currentUid !== uid)
            return;

        try {
            this._appFilter = await this._getAppFilter();
            this.emit('app-filter-changed');
        } catch (e) {
            // Log an error and keep the old app filter.
            logError(e, `Failed to get new MctAppFilter for uid ${Shell.util_get_uid()} on app-filter-changed`);
        }
    }

    get initialized() {
        return this._initialized;
    }

    // Calculate whether the given app (a Gio.DesktopAppInfo) should be shown
    // on the desktop, in search results, etc. The app should be shown if:
    //  - The .desktop file doesn’t say it should be hidden.
    //  - The executable from the .desktop file’s Exec line isn’t denied in
    //    the user’s parental controls.
    //  - None of the flatpak app IDs from the X-Flatpak and the
    //    X-Flatpak-RenamedFrom lines are denied in the user’s parental
    //    controls.
    shouldShowApp(appInfo) {
        // Quick decision?
        if (!appInfo.should_show())
            return false;

        // Are parental controls enabled (at configure time or runtime)?
        if (!HAVE_MALCONTENT || this._disabled)
            return true;

        // Have we finished initialising yet?
        if (!this.initialized) {
            console.debug(`Hiding app because parental controls not yet initialised: ${appInfo.get_id()}`);
            return false;
        }

        return this._appFilter.is_appinfo_allowed(appInfo);
    }
});
(uuay)sharedInternals.js!import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';

import {bindtextdomain} from 'gettext';

import * as Config from '../misc/config.js';

export class ExtensionBase {
    #gettextDomain;

    /**
     * Look up an extension by URL (usually 'import.meta.url')
     *
     * @param {string} url - a file:// URL
     */
    static lookupByURL(url) {
        if (!url.startsWith('file://'))
            return null;

        // Keep the last '/' from 'file://' to force an absolute path
        let path = url.slice(6);

        // Walk up the directory tree, looking for an extension with
        // the same UUID as a directory name.
        do {
            path = GLib.path_get_dirname(path);

            const dirName = GLib.path_get_basename(path);
            const extension = this.lookupByUUID(dirName);
            if (extension !== null)
                return extension;
        } while (path !== '/');

        return null;
    }

    /**
     * Look up an extension by UUID
     *
     * @param {string} _uuid
     */
    static lookupByUUID(_uuid) {
        throw new GObject.NotImplementedError();
    }

    /**
     * @param {object} metadata - metadata passed in when loading the extension
     */
    constructor(metadata) {
        if (this.constructor === ExtensionBase)
            throw new Error('ExtensionBase cannot be used directly.');

        if (!metadata)
            throw new Error(`${this.constructor.name} did not pass metadata to parent`);

        this.metadata = metadata;
        this.initTranslations();
    }

    /**
     * @type {string}
     */
    get uuid() {
        return this.metadata['uuid'];
    }

    /**
     * @type {Gio.File}
     */
    get dir() {
        return this.metadata['dir'];
    }

    /**
     * @type {string}
     */
    get path() {
        return this.metadata['path'];
    }

    /**
     * Get a GSettings object for schema, using schema files in
     * extensionsdir/schemas. If schema is omitted, it is taken
     * from metadata['settings-schema'].
     *
     * @param {string=} schema - the GSettings schema id
     *
     * @returns {Gio.Settings}
     */
    getSettings(schema) {
        schema ||= this.metadata['settings-schema'];

        // Expect USER extensions to have a schemas/ subfolder, otherwise assume a
        // SYSTEM extension that has been installed in the same prefix as the shell
        const schemaDir = this.dir.get_child('schemas');
        const defaultSource = Gio.SettingsSchemaSource.get_default();
        let schemaSource;
        if (schemaDir.query_exists(null)) {
            schemaSource = Gio.SettingsSchemaSource.new_from_directory(
                schemaDir.get_path(), defaultSource, false);
        } else {
            schemaSource = defaultSource;
        }

        const schemaObj = schemaSource.lookup(schema, true);
        if (!schemaObj)
            throw new Error(`Schema ${schema} could not be found for extension ${this.uuid}. Please check your installation`);

        return new Gio.Settings({settings_schema: schemaObj});
    }

    /**
     * Initialize Gettext to load translations from extensionsdir/locale. If
     * domain is not provided, it will be taken from metadata['gettext-domain']
     * if provided, or use the UUID
     *
     * @param {string=} domain - the gettext domain to use
     */
    initTranslations(domain) {
        domain ||= this.metadata['gettext-domain'] ?? this.uuid;

        // Expect USER extensions to have a locale/ subfolder, otherwise assume a
        // SYSTEM extension that has been installed in the same prefix as the shell
        const localeDir = this.dir.get_child('locale');
        if (localeDir.query_exists(null))
            bindtextdomain(domain, localeDir.get_path());
        else
            bindtextdomain(domain, Config.LOCALEDIR);

        this.#gettextDomain = domain;
    }

    /**
     * Translate `str` using the extension's gettext domain
     *
     * @param {string} str - the string to translate
     *
     * @returns {string} the translated string
     */
    gettext(str) {
        this.#checkGettextDomain('gettext');
        return GLib.dgettext(this.#gettextDomain, str);
    }

    /**
     * Translate `str` and choose plural form using the extension's
     * gettext domain
     *
     * @param {string} str - the string to translate
     * @param {string} strPlural - the plural form of the string
     * @param {number} n - the quantity for which translation is needed
     *
     * @returns {string} the translated string
     */
    ngettext(str, strPlural, n) {
        this.#checkGettextDomain('ngettext');
        return GLib.dngettext(this.#gettextDomain, str, strPlural, n);
    }

    /**
     * Translate `str` in the context of `context` using the extension's
     * gettext domain
     *
     * @param {string} context - context to disambiguate `str`
     * @param {string} str - the string to translate
     *
     * @returns {string} the translated string
     */
    pgettext(context, str) {
        this.#checkGettextDomain('pgettext');
        return GLib.dpgettext2(this.#gettextDomain, context, str);
    }

    /**
     * @param {string} func
     */
    #checkGettextDomain(func) {
        if (!this.#gettextDomain)
            throw new Error(`${func}() is used without calling initTranslations() first`);
    }
}

export class GettextWrapper {
    #url;
    #extensionClass;

    constructor(extensionClass, url) {
        this.#url = url;
        this.#extensionClass = extensionClass;
    }

    #detectUrl() {
        const basePath = '/gnome-shell/extensions/';

        // Search for an occurrence of an extension stack frame
        // Start at 1 because 0 is the stack frame of this function
        const [, ...stack] = new Error().stack.split('\n');
        const extensionLine = stack.find(
            line => line.includes(basePath));

        if (!extensionLine)
            return null;

        // The exact stack line differs depending on where the function
        // was called (function or module scope), and whether it's called
        // from a module or legacy import (file:// URI vs. plain path).
        //
        // We don't have to care about the exact composition, all we need
        // is a string that can be traversed as path and contains the UUID
        const path = extensionLine.slice(extensionLine.indexOf(basePath));
        return `file://${path}`;
    }

    #lookupExtension(funcName) {
        const url = this.#url ?? this.#detectUrl();
        const extension = this.#extensionClass.lookupByURL(url);
        if (!extension)
            throw new Error(`${funcName} can only be called from extensions`);
        return extension;
    }

    #gettext(str) {
        const extension = this.#lookupExtension('gettext');
        return extension.gettext(str);
    }

    #ngettext(str, strPlural, n) {
        const extension = this.#lookupExtension('ngettext');
        return extension.ngettext(str, strPlural, n);
    }

    #pgettext(context, str) {
        const extension = this.#lookupExtension('pgettext');
        return extension.pgettext(context, str);
    }

    defineTranslationFunctions() {
        return {
            /**
             * Translate `str` using the extension's gettext domain
             *
             * @param {string} str - the string to translate
             *
             * @returns {string} the translated string
             */
            gettext: this.#gettext.bind(this),

            /**
             * Translate `str` and choose plural form using the extension's
             * gettext domain
             *
             * @param {string} str - the string to translate
             * @param {string} strPlural - the plural form of the string
             * @param {number} n - the quantity for which translation is needed
             *
             * @returns {string} the translated string
             */
            ngettext: this.#ngettext.bind(this),

            /**
             * Translate `str` in the context of `context` using the extension's
             * gettext domain
             *
             * @param {string} context - context to disambiguate `str`
             * @param {string} str - the string to translate
             *
             * @returns {string} the translated string
             */
            pgettext: this.#pgettext.bind(this),
        };
    }
}
(uuay)darkMode.js9import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import St from 'gi://St';

import * as Main from '../main.js';

import {QuickToggle, SystemIndicator} from '../quickSettings.js';

const DarkModeToggle = GObject.registerClass(
class DarkModeToggle extends QuickToggle {
    _init() {
        super._init({
            title: _('Dark Style'),
            iconName: 'dark-mode-symbolic',
        });

        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.interface',
        });
        this._changedId = this._settings.connect('changed::color-scheme',
            () => this._sync());

        St.Settings.get().connect('notify::gtk-theme', () => this._sync());
        St.Settings.get().connect('notify::gtk-theme-variant', () => this._sync());

        this.connectObject(
            'destroy', () => this._settings.run_dispose(),
            'clicked', () => this._toggleMode(),
            this);
        this._sync();
    }

    _toggleMode() {
        Main.layoutManager.screenTransition.run();
        const preferDark = !this.checked;
        const {gtkTheme, gtkThemeVariant} = St.Settings.get();
        const themeVariant = gtkThemeVariant?.toLowerCase();
        this._settings.set_string('color-scheme',
            this.checked ? 'default' : 'prefer-dark');

        if (gtkTheme === 'Yaru')
            this._setYaruSettings(themeVariant, preferDark);
    }

    _setYaruSettings(themeVariant, preferDark) {
        const currentlyDark = themeVariant === 'dark' || themeVariant?.endsWith('-dark');
        if (currentlyDark)
            themeVariant = themeVariant.slice(0, -'-dark'.length);

        if (currentlyDark !== preferDark) {
            const newTheme = `Yaru${
                themeVariant ? `-${themeVariant}` : ''}${
                preferDark ? '-dark' : ''}`;

            this._settings.set_string('gtk-theme', newTheme);
            this._settings.set_string('icon-theme', newTheme);
        }

        const schemaSource = Gio.SettingsSchemaSource.get_default();
        const geditSchema = schemaSource.lookup('org.gnome.gedit.preferences.editor', true);

        if (geditSchema) {
            const geditSettings = Gio.Settings.new_full(geditSchema, null, null);
            const geditScheme = geditSettings.get_user_value('scheme')?.unpack();

            if (geditScheme?.startsWith('Yaru') &&
                geditScheme.endsWith('-dark') !== preferDark)
                geditSettings.set_string('scheme', `Yaru${preferDark ? '-dark' : ''}`);
        }
    }

    _sync() {
        const colorScheme = this._settings.get_string('color-scheme');
        const {gtkTheme, gtkThemeVariant} = St.Settings.get();
        let checked = colorScheme === 'prefer-dark';
        if (gtkTheme === 'Yaru' && !gtkThemeVariant?.endsWith('dark'))
            checked = false;
        if (this.checked !== checked)
            this.set({checked});
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this.quickSettingsItems.push(new DarkModeToggle());
    }
});
(uuay)dbusUtils.js�import Gio from 'gi://Gio';
import GLib from 'gi://GLib';

import * as Config from './config.js';

let _ifaceResource = null;

/**
 * @private
 */
function _ensureIfaceResource() {
    if (_ifaceResource)
        return;

    // don't use global.datadir so the method is usable from tests/tools
    let dir = GLib.getenv('GNOME_SHELL_DATADIR') || Config.PKGDATADIR;
    let path = `${dir}/gnome-shell-dbus-interfaces.gresource`;
    _ifaceResource = Gio.Resource.load(path);
    _ifaceResource._register();
}

/**
 * @param {string} iface the interface name
 * @returns {string | null} the XML string or null if it is not found
 */
export function loadInterfaceXML(iface) {
    _ensureIfaceResource();

    let uri = `resource:///org/gnome/shell/dbus-interfaces/${iface}.xml`;
    let f = Gio.File.new_for_uri(uri);

    try {
        let [ok_, bytes] = f.load_contents(null);
        return new TextDecoder().decode(bytes);
    } catch (e) {
        log(`Failed to load D-Bus interface ${iface}`);
    }

    return null;
}

/**
 * @param {string} iface the interface name
 * @param {string} ifaceFile the interface filename
 * @returns {string | null} the XML string or null if it is not found
 */
export function loadSubInterfaceXML(iface, ifaceFile) {
    let xml = loadInterfaceXML(ifaceFile);
    if (!xml)
        return null;

    let ifaceStartTag = `<interface name="${iface}">`;
    let ifaceStopTag = '</interface>';
    let ifaceStartIndex = xml.indexOf(ifaceStartTag);
    let ifaceEndIndex = xml.indexOf(ifaceStopTag, ifaceStartIndex + 1) + ifaceStopTag.length;

    let xmlHeader = '<!DOCTYPE node PUBLIC\n' +
        '\'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\'\n' +
        '\'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\'>\n' +
        '<node>\n';
    let xmlFooter = '</node>';

    return (
        xmlHeader +
        xml.substr(ifaceStartIndex, ifaceEndIndex - ifaceStartIndex) +
        xmlFooter);
}
(uuay)ripples.js// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import St from 'gi://St';

// Shamelessly copied from the layout "hotcorner" ripples implementation
export class Ripples {
    constructor(px, py, styleClass) {
        this._x = 0;
        this._y = 0;

        this._px = px;
        this._py = py;

        this._ripple1 = new St.BoxLayout({
            style_class: styleClass,
            opacity: 0,
            can_focus: false,
            reactive: false,
            visible: false,
        });
        this._ripple1.set_pivot_point(px, py);

        this._ripple2 = new St.BoxLayout({
            style_class: styleClass,
            opacity: 0,
            can_focus: false,
            reactive: false,
            visible: false,
        });
        this._ripple2.set_pivot_point(px, py);

        this._ripple3 = new St.BoxLayout({
            style_class: styleClass,
            opacity: 0,
            can_focus: false,
            reactive: false,
            visible: false,
        });
        this._ripple3.set_pivot_point(px, py);
    }

    destroy() {
        this._ripple1.destroy();
        this._ripple2.destroy();
        this._ripple3.destroy();
    }

    _animRipple(ripple, delay, duration, startScale, startOpacity, finalScale) {
        // We draw a ripple by using a source image and animating it scaling
        // outwards and fading away. We want the ripples to move linearly
        // or it looks unrealistic, but if the opacity of the ripple goes
        // linearly to zero it fades away too quickly, so we use a separate
        // tween to give a non-linear curve to the fade-away and make
        // it more visible in the middle section.

        ripple.x = this._x;
        ripple.y = this._y;
        ripple.visible = true;
        ripple.opacity = 255 * Math.sqrt(startOpacity);
        ripple.scale_x = ripple.scale_y = startScale;
        ripple.set_translation(-this._px * ripple.width, -this._py * ripple.height, 0.0);

        ripple.ease({
            opacity: 0,
            delay,
            duration,
            mode: Clutter.AnimationMode.EASE_IN_QUAD,
        });
        ripple.ease({
            scale_x: finalScale,
            scale_y: finalScale,
            delay,
            duration,
            mode: Clutter.AnimationMode.LINEAR,
            onComplete: () => (ripple.visible = false),
        });
    }

    addTo(stage) {
        if (this._stage !== undefined)
            throw new Error('Ripples already added');

        this._stage = stage;
        this._stage.add_child(this._ripple1);
        this._stage.add_child(this._ripple2);
        this._stage.add_child(this._ripple3);
    }

    playAnimation(x, y) {
        if (this._stage === undefined)
            throw new Error('Ripples not added');

        this._x = x;
        this._y = y;

        this._stage.set_child_above_sibling(this._ripple1, null);
        this._stage.set_child_above_sibling(this._ripple2, this._ripple1);
        this._stage.set_child_above_sibling(this._ripple3, this._ripple2);

        // Show three concentric ripples expanding outwards; the exact
        // parameters were found by trial and error, so don't look
        // for them to make perfect sense mathematically

        //                              delay  time   scale opacity => scale
        this._animRipple(this._ripple1,   0,    830,   0.25,  1.0,     1.5);
        this._animRipple(this._ripple2,  50,   1000,   0.0,   0.7,     1.25);
        this._animRipple(this._ripple3, 350,   1000,   0.0,   0.3,     1);
    }
}
(uuay)ctrlAltTab.js// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Main from './main.js';
import * as SwitcherPopup from './switcherPopup.js';
import * as Params from '../misc/params.js';

const POPUP_APPICON_SIZE = 96;

export const SortGroup = {
    TOP:    0,
    MIDDLE: 1,
    BOTTOM: 2,
};

export class CtrlAltTabManager {
    constructor() {
        this._items = [];
        this.addGroup(global.window_group,
            _('Windows'),
            'shell-focus-windows-symbolic', {
                sortGroup: SortGroup.TOP,
                focusCallback: this._focusWindows.bind(this),
            });
    }

    addGroup(root, name, icon, params) {
        const item = Params.parse(params, {
            sortGroup: SortGroup.MIDDLE,
            proxy: root,
            focusCallback: null,
        });

        item.root = root;
        item.name = name;
        item.iconName = icon;

        this._items.push(item);
        root.connect('destroy', () => this.removeGroup(root));
        if (root instanceof St.Widget)
            global.focus_manager.add_group(root);
    }

    removeGroup(root) {
        if (root instanceof St.Widget)
            global.focus_manager.remove_group(root);
        for (let i = 0; i < this._items.length; i++) {
            if (this._items[i].root === root) {
                this._items.splice(i, 1);
                return;
            }
        }
    }

    focusGroup(item, timestamp) {
        if (item.focusCallback)
            item.focusCallback(timestamp);
        else
            item.root.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
    }

    // Sort the items into a consistent order; panel first, tray last,
    // and everything else in between, sorted by X coordinate, so that
    // they will have the same left-to-right ordering in the
    // Ctrl-Alt-Tab dialog as they do onscreen.
    _sortItems(a, b) {
        if (a.sortGroup !== b.sortGroup)
            return a.sortGroup - b.sortGroup;

        let [ax] = a.proxy.get_transformed_position();
        let [bx] = b.proxy.get_transformed_position();

        return ax - bx;
    }

    popup(backward, binding, mask) {
        // Start with the set of focus groups that are currently mapped
        let items = this._items.filter(item => item.proxy.mapped);

        // And add the windows metacity would show in its Ctrl-Alt-Tab list
        if (Main.sessionMode.hasWindows && !Main.overview.visible) {
            let display = global.display;
            let workspaceManager = global.workspace_manager;
            let activeWorkspace = workspaceManager.get_active_workspace();
            let windows = display.get_tab_list(Meta.TabList.DOCKS,
                activeWorkspace);
            let windowTracker = Shell.WindowTracker.get_default();
            let textureCache = St.TextureCache.get_default();
            for (let i = 0; i < windows.length; i++) {
                let icon = null;
                let iconName = null;
                if (windows[i].get_window_type() === Meta.WindowType.DESKTOP) {
                    iconName = 'shell-focus-desktop-symbolic';
                } else {
                    let app = windowTracker.get_window_app(windows[i]);
                    if (app) {
                        icon = app.create_icon_texture(POPUP_APPICON_SIZE);
                    } else {
                        icon = new St.Icon({
                            gicon: textureCache.bind_cairo_surface_property(windows[i], 'icon'),
                            icon_size: POPUP_APPICON_SIZE,
                        });
                    }
                }

                items.push({
                    name: windows[i].title,
                    proxy: windows[i].get_compositor_private(),
                    focusCallback: timestamp => {
                        Main.activateWindow(windows[i], timestamp);
                    },
                    iconActor: icon,
                    iconName,
                    sortGroup: SortGroup.MIDDLE,
                });
            }
        }

        if (!items.length)
            return;

        items.sort(this._sortItems.bind(this));

        if (!this._popup) {
            this._popup = new CtrlAltTabPopup(items);
            this._popup.show(backward, binding, mask);

            this._popup.connect('destroy', () => {
                this._popup = null;
            });
        }
    }

    _focusWindows(timestamp) {
        global.display.focus_default_window(timestamp);
    }
}

const CtrlAltTabPopup = GObject.registerClass(
class CtrlAltTabPopup extends SwitcherPopup.SwitcherPopup {
    _init(items) {
        super._init(items);

        this._switcherList = new CtrlAltTabSwitcher(this._items);
    }

    _keyPressHandler(keysym, action) {
        if (action === Meta.KeyBindingAction.SWITCH_PANELS)
            this._select(this._next());
        else if (action === Meta.KeyBindingAction.SWITCH_PANELS_BACKWARD)
            this._select(this._previous());
        else if (keysym === Clutter.KEY_Left)
            this._select(this._previous());
        else if (keysym === Clutter.KEY_Right)
            this._select(this._next());
        else
            return Clutter.EVENT_PROPAGATE;

        return Clutter.EVENT_STOP;
    }

    _finish(time) {
        super._finish(time);
        Main.ctrlAltTabManager.focusGroup(this._items[this._selectedIndex], time);
    }
});

const CtrlAltTabSwitcher = GObject.registerClass(
class CtrlAltTabSwitcher extends SwitcherPopup.SwitcherList {
    _init(items) {
        super._init(true);

        for (let i = 0; i < items.length; i++)
            this._addIcon(items[i]);
    }

    _addIcon(item) {
        const box = new St.BoxLayout({
            style_class: 'alt-tab-app',
            vertical: true,
        });

        let icon = item.iconActor;
        if (!icon) {
            icon = new St.Icon({
                icon_name: item.iconName,
                icon_size: POPUP_APPICON_SIZE,
            });
        }
        box.add_child(icon);

        let text = new St.Label({
            text: item.name,
            x_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(text);

        this.addItem(box, text);
    }
});
(uuay)screenShield.js]f// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import AccountsService from 'gi://AccountsService';
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Signals from '../misc/signals.js';

import * as GnomeSession from '../misc/gnomeSession.js';
import * as OVirt from '../gdm/oVirt.js';
import * as LoginManager from '../misc/loginManager.js';
import * as Lightbox from './lightbox.js';
import * as Main from './main.js';
import * as Overview from './overview.js';
import * as MessageTray from './messageTray.js';
import * as ShellDBus from './shellDBus.js';
import * as SmartcardManager from '../misc/smartcardManager.js';

import {adjustAnimationTime} from '../misc/animationUtils.js';

const SCREENSAVER_SCHEMA = 'org.gnome.desktop.screensaver';
const LOCK_ENABLED_KEY = 'lock-enabled';
const LOCK_DELAY_KEY = 'lock-delay';
const SUSPEND_LOCK_ENABLED_KEY = 'ubuntu-lock-on-suspend';

const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown';
const DISABLE_LOCK_KEY = 'disable-lock-screen';

const LOCKED_STATE_STR = 'screenShield.locked';

const LOGIN_SCREEN_SCHEMA = 'com.ubuntu.login-screen';
const LOGIN_SCREEN_BACKGROUND_COLOR_KEY = 'background-color';
const LOGIN_SCREEN_BACKGROUND_PICTURE_URI_KEY = 'background-picture-uri';
const LOGIN_SCREEN_BACKGROUND_REPEAT_KEY = 'background-repeat';
const LOGIN_SCREEN_BACKGROUND_SIZE_KEY = 'background-size';

// ScreenShield animation time
// - STANDARD_FADE_TIME is used when the session goes idle
// - MANUAL_FADE_TIME is used for lowering the shield when asked by the user,
//   or when cancelling the dialog
// - CURTAIN_SLIDE_TIME is used when raising the shield before unlocking
const STANDARD_FADE_TIME = 10000;
const MANUAL_FADE_TIME = 300;
const CURTAIN_SLIDE_TIME = 300;

/**
 * If you are setting org.gnome.desktop.session.idle-delay directly in dconf,
 * rather than through System Settings, you also need to set
 * org.gnome.settings-daemon.plugins.power.sleep-display-ac and
 * org.gnome.settings-daemon.plugins.power.sleep-display-battery to the same value.
 * This will ensure that the screen blanks at the right time when it fades out.
 * https://bugzilla.gnome.org/show_bug.cgi?id=668703 explains the dependency.
 */
export class ScreenShield extends Signals.EventEmitter {
    constructor() {
        super();

        this.actor = Main.layoutManager.screenShieldGroup;

        this._lockScreenState = MessageTray.State.HIDDEN;
        this._lockScreenGroup = new St.Widget({
            x_expand: true,
            y_expand: true,
            reactive: true,
            can_focus: true,
            name: 'lockScreenGroup',
            visible: false,
        });

        this._lockDialogGroup = new St.Widget({
            x_expand: true,
            y_expand: true,
            reactive: true,
            can_focus: true,
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
            name: 'lockDialogGroup',
        });

        this.actor.add_child(this._lockScreenGroup);
        this.actor.add_child(this._lockDialogGroup);

        this._presence = new GnomeSession.Presence((proxy, error) => {
            if (error) {
                logError(error, 'Error while reading gnome-session presence');
                return;
            }

            this._onStatusChanged(proxy.status);
        });
        this._presence.connectSignal('StatusChanged', (proxy, senderName, [status]) => {
            this._onStatusChanged(status);
        });

        this._screenSaverDBus = new ShellDBus.ScreenSaverDBus(this);

        this._smartcardManager = SmartcardManager.getSmartcardManager();
        this._smartcardManager.connect('smartcard-inserted',
            (manager, token) => {
                if (this._isLocked && token.UsedToLogin)
                    this._activateDialog();
            });

        this._credentialManagers = {};
        this.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager());

        this._loginManager = LoginManager.getLoginManager();
        this._loginManager.connect('prepare-for-sleep',
            this._prepareForSleep.bind(this));

        this._loginSession = null;
        this._getLoginSession();

        this._settings = new Gio.Settings({schema_id: SCREENSAVER_SCHEMA});
        this._settings.connect(`changed::${LOCK_ENABLED_KEY}`, this._syncInhibitor.bind(this));
        this._settings.connect(`changed::${SUSPEND_LOCK_ENABLED_KEY}`, this._syncInhibitor.bind(this));

        this._lockSettings = new Gio.Settings({schema_id: LOCKDOWN_SCHEMA});
        this._lockSettings.connect(`changed::${DISABLE_LOCK_KEY}`, this._syncInhibitor.bind(this));

        this._loginScreenSettings = new Gio.Settings({schema_id: LOGIN_SCREEN_SCHEMA});
        [
            LOGIN_SCREEN_BACKGROUND_COLOR_KEY,
            LOGIN_SCREEN_BACKGROUND_PICTURE_URI_KEY,
            LOGIN_SCREEN_BACKGROUND_REPEAT_KEY,
            LOGIN_SCREEN_BACKGROUND_SIZE_KEY,
        ].forEach(schema => this._loginScreenSettings.connect(`changed::${schema}`,
            () => this._refreshBackground()));
        this._refreshBackground();

        this._isModal = false;
        this._isGreeter = false;
        this._isActive = false;
        this._isLocked = false;
        this._inUnlockAnimation = false;
        this._inhibited = false;
        this._activationTime = 0;
        this._becameActiveId = 0;
        this._lockTimeoutId = 0;

        // The "long" lightbox is used for the longer (20 seconds) fade from session
        // to idle status, the "short" is used for quickly fading to black when locking
        // manually
        this._longLightbox = new Lightbox.Lightbox(Main.uiGroup, {
            inhibitEvents: true,
            fadeFactor: 1,
        });
        this._longLightbox.connect('notify::active', this._onLongLightbox.bind(this));
        this._shortLightbox = new Lightbox.Lightbox(Main.uiGroup, {
            inhibitEvents: true,
            fadeFactor: 1,
        });
        this._shortLightbox.connect('notify::active', this._onShortLightbox.bind(this));

        this.idleMonitor = global.backend.get_core_idle_monitor();
        this._cursorTracker = Meta.CursorTracker.get_for_display(global.display);

        this._syncInhibitor();
    }

    async _getLoginSession() {
        this._loginSession = await this._loginManager.getCurrentSessionProxy();
        this._loginSession.connectSignal('Lock',
            () => this.lock(false));
        this._loginSession.connectSignal('Unlock',
            () => this.deactivate(false));
        this._loginSession.connect('g-properties-changed',
            () => this._syncInhibitor());
        this._syncInhibitor();
    }

    _setActive(active) {
        let prevIsActive = this._isActive;
        this._isActive = active;

        if (prevIsActive !== this._isActive)
            this.emit('active-changed');

        this._syncInhibitor();
    }

    _setLocked(locked) {
        let prevIsLocked = this._isLocked;
        this._isLocked = locked;

        if (prevIsLocked !== this._isLocked)
            this.emit('locked-changed');

        if (this._loginSession)
            this._loginSession.SetLockedHintAsync(locked).catch(logError);
    }

    _activateDialog() {
        if (this._isLocked) {
            this._ensureUnlockDialog(true /* allowCancel */);
            this._dialog.activate();
        } else {
            this.deactivate(true /* animate */);
        }
    }

    _maybeCancelDialog() {
        if (!this._dialog)
            return;

        this._dialog.cancel();
        if (this._isGreeter) {
            // LoginDialog.cancel() will grab the key focus
            // on its own, so ensure it stays on lock screen
            // instead
            this._dialog.grab_key_focus();
        }
    }

    _becomeModal() {
        if (this._isModal)
            return true;

        let grab = Main.pushModal(Main.uiGroup, {actionMode: Shell.ActionMode.LOCK_SCREEN});

        // We expect at least a keyboard grab here
        this._isModal = (grab.get_seat_state() & Clutter.GrabState.KEYBOARD) !== 0;
        if (this._isModal)
            this._grab = grab;
        else
            Main.popModal(grab);

        return this._isModal;
    }

    _refreshBackground() {
        const inlineStyle = [];

        const getSetting = s => this._loginScreenSettings.get_string(s);
        const backgroundColor = getSetting(LOGIN_SCREEN_BACKGROUND_COLOR_KEY);
        const backgroundPictureUri = getSetting(LOGIN_SCREEN_BACKGROUND_PICTURE_URI_KEY);
        const backgroundRepeat = getSetting(LOGIN_SCREEN_BACKGROUND_REPEAT_KEY);
        const backgroundSize = getSetting(LOGIN_SCREEN_BACKGROUND_SIZE_KEY);

        if (backgroundColor && !backgroundColor.includes('rgba'))
            inlineStyle.push(`background-color: ${backgroundColor}`);
        if (backgroundPictureUri)
            inlineStyle.push(`background-image: url("${backgroundPictureUri}")`);
        if (backgroundRepeat !== 'default')
            inlineStyle.push(`background-repeat: ${backgroundRepeat}`);
        if (backgroundSize !== 'default')
            inlineStyle.push(`background-size: ${backgroundSize}`);

        const lockDialogGroupStyle = inlineStyle.join('; ') || null;
        this._lockDialogGroup.set_style(lockDialogGroupStyle);
        this._dialog?.set_style(lockDialogGroupStyle ? `
            background-image: none;
            background-color: transparent;` : null);
    }

    async _syncInhibitor() {
        const lockEnabled = this._settings.get_boolean(LOCK_ENABLED_KEY) ||
                            this._settings.get_boolean(SUSPEND_LOCK_ENABLED_KEY);
        const lockLocked = this._lockSettings.get_boolean(DISABLE_LOCK_KEY);
        const inhibit = !!this._loginSession && this._loginSession.Active &&
                         !this._isActive && lockEnabled && !lockLocked &&
                         !!Main.sessionMode.unlockDialog;

        if (inhibit === this._inhibited)
            return;

        this._inhibited = inhibit;

        this._inhibitCancellable?.cancel();
        this._inhibitCancellable = new Gio.Cancellable();

        if (inhibit) {
            try {
                this._inhibitor = await this._loginManager.inhibit(
                    _('GNOME needs to lock the screen'),
                    this._inhibitCancellable);
            } catch (e) {
                if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                    log('Failed to inhibit suspend: %s'.format(e.message));
            }
        } else {
            this._inhibitor?.close(null);
            this._inhibitor = null;
        }
    }

    _prepareForSleep(loginManager, aboutToSuspend) {
        if (aboutToSuspend) {
            if (this._settings.get_boolean(SUSPEND_LOCK_ENABLED_KEY))
                this.lock(true);
        } else {
            this._wakeUpScreen();
        }
    }

    _onStatusChanged(status) {
        if (status !== GnomeSession.PresenceStatus.IDLE)
            return;

        this._maybeCancelDialog();

        if (this._longLightbox.visible) {
            // We're in the process of showing.
            return;
        }

        if (!this._becomeModal()) {
            // We could not become modal, so we can't activate the
            // screenshield. The user is probably very upset at this
            // point, but any application using global grabs is broken
            // Just tell them to stop using this app
            //
            // XXX: another option is to kick the user into the gdm login
            // screen, where we're not affected by grabs
            Main.notifyError(
                _('Unable to lock'),
                _('Lock was blocked by an app'));
            return;
        }

        if (this._activationTime === 0)
            this._activationTime = GLib.get_monotonic_time();

        let shouldLock = this._settings.get_boolean(LOCK_ENABLED_KEY) && !this._isLocked;

        if (shouldLock) {
            let lockTimeout = Math.max(
                adjustAnimationTime(STANDARD_FADE_TIME),
                this._settings.get_uint(LOCK_DELAY_KEY) * 1000);
            this._lockTimeoutId = GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                lockTimeout,
                () => {
                    this._lockTimeoutId = 0;
                    this.lock(false);
                    return GLib.SOURCE_REMOVE;
                });
            GLib.Source.set_name_by_id(this._lockTimeoutId, '[gnome-shell] this.lock');
        }

        this._activateFade(this._longLightbox, STANDARD_FADE_TIME);
    }

    _activateFade(lightbox, time) {
        Main.uiGroup.set_child_above_sibling(lightbox, null);
        lightbox.lightOn(time);

        if (this._becameActiveId === 0)
            this._becameActiveId = this.idleMonitor.add_user_active_watch(this._onUserBecameActive.bind(this));
    }

    _onUserBecameActive() {
        // This function gets called here when the user becomes active
        // after we activated a lightbox
        // There are two possibilities here:
        // - we're called when already locked; we just go back to the lock screen curtain
        // - we're called because the session is IDLE but before the lightbox
        //   is fully shown; at this point isActive is false, so we just hide
        //   the lightbox, reset the activationTime and go back to the unlocked
        //   desktop
        //   using deactivate() is a little of overkill, but it ensures we
        //   don't forget of some bit like modal, DBus properties or idle watches
        //
        // Note: if the (long) lightbox is shown then we're necessarily
        // active, because we call activate() without animation.

        this.idleMonitor.remove_watch(this._becameActiveId);
        this._becameActiveId = 0;

        if (this._isLocked) {
            this._longLightbox.lightOff();
            this._shortLightbox.lightOff();
        } else {
            this.deactivate(false);
        }
    }

    _onLongLightbox(lightBox) {
        if (lightBox.active)
            this.activate(false);
    }

    _onShortLightbox(lightBox) {
        if (lightBox.active)
            this._completeLockScreenShown();
    }

    showDialog() {
        if (!this._becomeModal()) {
            // In the login screen, this is a hard error. Fail-whale
            const error = new GLib.Error(
                Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED,
                'Could not acquire modal grab for the login screen. Aborting login process.');
            global.context.terminate_with_error(error);
        }

        this.actor.show();
        this._isGreeter = Main.sessionMode.isGreeter;
        this._isLocked = true;
        this._ensureUnlockDialog(true);
    }

    _hideLockScreenComplete() {
        this._lockScreenState = MessageTray.State.HIDDEN;
        this._lockScreenGroup.hide();

        if (this._dialog) {
            this._dialog.grab_key_focus();
            this._dialog.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
        }
    }

    _showPointer() {
        this._cursorTracker.set_pointer_visible(true);

        if (this._motionId) {
            global.stage.disconnect(this._motionId);
            this._motionId = 0;
        }
    }

    _hidePointerUntilMotion() {
        this._motionId = global.stage.connect('captured-event', (stage, event) => {
            if (event.type() === Clutter.EventType.MOTION)
                this._showPointer();

            return Clutter.EVENT_PROPAGATE;
        });
        this._cursorTracker.set_pointer_visible(false);
    }

    _hideLockScreen(animate) {
        if (this._lockScreenState === MessageTray.State.HIDDEN)
            return;

        this._lockScreenState = MessageTray.State.HIDING;

        this._lockDialogGroup.remove_all_transitions();

        // Animate the lock screen out of screen
        // if velocity is not specified (i.e. we come here from pressing ESC),
        // use the same speed regardless of original position
        // if velocity is specified, it's in pixels per milliseconds
        const height = global.stage.height;
        const delta = height + this._lockDialogGroup.translation_y;
        const velocity = height / CURTAIN_SLIDE_TIME;
        const duration = animate ? delta / velocity : 0;

        this._lockDialogGroup.ease({
            translation_y: -height,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this._hideLockScreenComplete(),
        });

        this._showPointer();
    }

    _ensureUnlockDialog(allowCancel) {
        if (!this._dialog) {
            let constructor = Main.sessionMode.unlockDialog;
            if (!constructor) {
                // This session mode has no locking capabilities
                this.deactivate(true);
                return false;
            }

            this._dialog = new constructor(this._lockDialogGroup);
            this._refreshBackground();

            if (!this._dialog.open()) {
                // This is kind of an impossible error: we're already modal
                // by the time we reach this...
                log('Could not open login dialog: failed to acquire grab');
                this.deactivate(true);
                return false;
            }

            this._dialog.connect('failed', this._onUnlockFailed.bind(this));
            this._wakeUpScreenId = this._dialog.connect(
                'wake-up-screen', this._wakeUpScreen.bind(this));
        }

        this._dialog.allowCancel = allowCancel;
        this._dialog.grab_key_focus();
        return true;
    }

    _onUnlockFailed() {
        this._resetLockScreen({
            animateLockScreen: true,
            fadeToBlack: false,
        });
    }

    _resetLockScreen(params) {
        // Don't reset the lock screen unless it is completely hidden
        // This prevents the shield going down if the lock-delay timeout
        // fires while the user is dragging (which has the potential
        // to confuse our state)
        if (this._lockScreenState !== MessageTray.State.HIDDEN)
            return;

        this._lockScreenGroup.show();
        this._lockScreenState = MessageTray.State.SHOWING;

        let fadeToBlack = params.fadeToBlack;

        if (params.animateLockScreen) {
            this._lockDialogGroup.translation_y = -global.screen_height;
            this._lockDialogGroup.remove_all_transitions();
            this._lockDialogGroup.ease({
                translation_y: 0,
                duration: Overview.ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this._lockScreenShown({fadeToBlack, animateFade: true});
                },
            });
        } else {
            this._lockDialogGroup.translation_y = 0;
            this._lockScreenShown({fadeToBlack, animateFade: false});
        }

        this._dialog.grab_key_focus();
    }

    _lockScreenShown(params) {
        this._hidePointerUntilMotion();

        this._lockScreenState = MessageTray.State.SHOWN;

        if (params.fadeToBlack && params.animateFade) {
            // Take a beat

            let id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, MANUAL_FADE_TIME, () => {
                this._activateFade(this._shortLightbox, MANUAL_FADE_TIME);
                return GLib.SOURCE_REMOVE;
            });
            GLib.Source.set_name_by_id(id, '[gnome-shell] this._activateFade');
        } else {
            if (params.fadeToBlack)
                this._activateFade(this._shortLightbox, 0);

            this._completeLockScreenShown();
        }
    }

    _completeLockScreenShown() {
        this._setActive(true);
        this.emit('lock-screen-shown');
    }

    _wakeUpScreen() {
        if (!this.active)
            return; // already woken up, or not yet asleep
        this._onUserBecameActive();
        this.emit('wake-up-screen');
    }

    get locked() {
        return this._isLocked;
    }

    get active() {
        return this._isActive;
    }

    get activationTime() {
        return this._activationTime;
    }

    deactivate(animate) {
        if (this._dialog)
            this._dialog.finish(() => this._continueDeactivate(animate));
        else
            this._continueDeactivate(animate);
    }

    _continueDeactivate(animate) {
        this._hideLockScreen(animate);

        if (Main.sessionMode.currentMode === 'unlock-dialog')
            Main.sessionMode.popMode('unlock-dialog');

        this.emit('wake-up-screen');

        if (this._isGreeter) {
            // We don't want to "deactivate" any more than
            // this. In particular, we don't want to drop
            // the modal, hide ourselves or destroy the dialog
            // But we do want to set isActive to false, so that
            // gnome-session will reset the idle counter, and
            // gnome-settings-daemon will stop blanking the screen

            this._activationTime = 0;
            this._setActive(false);
            return;
        }

        if (this._dialog && !this._isGreeter)
            this._dialog.popModal();

        if (this._isModal) {
            Main.popModal(this._grab);
            this._grab = null;
            this._isModal = false;
        }

        this._longLightbox.lightOff();
        this._shortLightbox.lightOff();

        this._lockDialogGroup.ease({
            translation_y: -global.screen_height,
            duration: Overview.ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this._completeDeactivate(),
        });
    }

    _completeDeactivate() {
        if (this._dialog) {
            this._dialog.destroy();
            this._dialog = null;
        }

        this.actor.hide();

        if (this._becameActiveId !== 0) {
            this.idleMonitor.remove_watch(this._becameActiveId);
            this._becameActiveId = 0;
        }

        if (this._lockTimeoutId !== 0) {
            GLib.source_remove(this._lockTimeoutId);
            this._lockTimeoutId = 0;
        }

        this._activationTime = 0;
        this._setActive(false);
        this._setLocked(false);
        global.set_runtime_state(LOCKED_STATE_STR, null);
    }

    activate(animate) {
        if (this._activationTime === 0)
            this._activationTime = GLib.get_monotonic_time();

        if (!this._ensureUnlockDialog(true))
            return;

        this.actor.show();

        if (Main.sessionMode.currentMode !== 'unlock-dialog') {
            this._isGreeter = Main.sessionMode.isGreeter;
            if (!this._isGreeter)
                Main.sessionMode.pushMode('unlock-dialog');
        }

        this._resetLockScreen({
            animateLockScreen: animate,
            fadeToBlack: true,
        });
        // On wayland, a crash brings down the entire session, so we don't
        // need to defend against being restarted unlocked
        if (!Meta.is_wayland_compositor())
            global.set_runtime_state(LOCKED_STATE_STR, GLib.Variant.new('b', true));

        // We used to set isActive and emit active-changed here,
        // but now we do that from lockScreenShown, which means
        // there is a 0.3 seconds window during which the lock
        // screen is effectively visible and the screen is locked, but
        // the DBus interface reports the screensaver is off.
        // This is because when we emit ActiveChanged(true),
        // gnome-settings-daemon blanks the screen, and we don't want
        // blank during the animation.
        // This is not a problem for the idle fade case, because we
        // activate without animation in that case.
    }

    addCredentialManager(serviceName, credentialManager) {
        if (this._credentialManagers[serviceName])
            return;

        this._credentialManagers[serviceName] = credentialManager;
        credentialManager.connectObject('user-authenticated', () => {
            if (this._isLocked)
                this._activateDialog();
        }, this);
    }

    removeCredentialManager(serviceName) {
        let credentialManager = this._credentialManagers[serviceName];
        if (!credentialManager)
            return;

        credentialManager.disconnectObject(this);
        delete this._credentialManagers[serviceName];
    }

    lock(animate) {
        if (this._lockSettings.get_boolean(DISABLE_LOCK_KEY)) {
            log('Screen lock is locked down, not locking'); // lock, lock - who's there?
            return;
        }

        // Warn the user if we can't become modal
        if (!this._becomeModal()) {
            Main.notifyError(
                _('Unable to lock'),
                _('Lock was blocked by an app'));
            return;
        }

        // Clear the clipboard - otherwise, its contents may be leaked
        // to unauthorized parties by pasting into the unlock dialog's
        // password entry and unmasking the entry
        St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, '');
        St.Clipboard.get_default().set_text(St.ClipboardType.PRIMARY, '');

        let userManager = AccountsService.UserManager.get_default();
        let user = userManager.get_user(GLib.get_user_name());

        this.activate(animate);

        const lock = this._isGreeter
            ? true
            : user.password_mode !== AccountsService.UserPasswordMode.NONE;
        this._setLocked(lock);
    }

    // If the previous shell crashed, and gnome-session restarted us, then re-lock
    lockIfWasLocked() {
        if (!this._settings.get_boolean(LOCK_ENABLED_KEY))
            return;
        let wasLocked = global.get_runtime_state('b', LOCKED_STATE_STR);
        if (wasLocked === null)
            return;
        const laters = global.compositor.get_laters();
        laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
            this.lock(false);
            return GLib.SOURCE_REMOVE;
        });
    }
}
(uuay)notificationDaemon.js�U// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GdkPixbuf from 'gi://GdkPixbuf';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';

import * as Config from '../misc/config.js';
import * as Main from './main.js';
import * as MessageTray from './messageTray.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';
import {NotificationErrors, NotificationError} from '../misc/dbusErrors.js';

const FdoNotificationsIface = loadInterfaceXML('org.freedesktop.Notifications');

/** @enum {number} */
const NotificationClosedReason = {
    EXPIRED: 1,
    DISMISSED: 2,
    APP_CLOSED: 3,
    UNDEFINED: 4,
};

/** @enum {number} */
const Urgency = {
    LOW: 0,
    NORMAL: 1,
    CRITICAL: 2,
};

class FdoNotificationDaemon {
    constructor() {
        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(FdoNotificationsIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/freedesktop/Notifications');

        this._sourcesForApp = new Map();
        this._sourceForPidAndName = new Map();
        this._notifications = new Map();

        this._nextNotificationId = 1;
    }

    _imageForNotificationData(hints) {
        if (hints['image-data']) {
            const [
                width, height, rowStride, hasAlpha,
                bitsPerSample, nChannels_, data,
            ] = hints['image-data'];
            return Shell.util_create_pixbuf_from_data(data,
                GdkPixbuf.Colorspace.RGB,
                hasAlpha,
                bitsPerSample,
                width,
                height,
                rowStride);
        } else if (hints['image-path']) {
            return this._iconForNotificationData(hints['image-path']);
        }
        return null;
    }

    _iconForNotificationData(icon) {
        if (icon) {
            if (icon.substr(0, 7) === 'file://')
                return new Gio.FileIcon({file: Gio.File.new_for_uri(icon)});
            else if (icon[0] === '/')
                return new Gio.FileIcon({file: Gio.File.new_for_path(icon)});
            else
                return new Gio.ThemedIcon({name: icon});
        }
        return null;
    }

    _getApp(pid, appId, appName) {
        const appSys = Shell.AppSystem.get_default();
        let app;

        app = Shell.WindowTracker.get_default().get_app_from_pid(pid);
        if (!app && appId)
            app = appSys.lookup_app(`${appId}.desktop`);

        if (!app)
            app = appSys.lookup_app(`${appName}.desktop`);

        return app;
    }

    // Returns the source associated with an app.
    //
    // If no existing source is found a new one is created.
    _getSourceForApp(sender, app) {
        let source = this._sourcesForApp.get(app);

        if (source)
            return source;

        source = new FdoNotificationDaemonSource(sender, app);

        if (app) {
            this._sourcesForApp.set(app, source);
            source.connect('destroy', () => {
                this._sourcesForApp.delete(app);
            });
        }

        Main.messageTray.add(source);
        return source;
    }

    // Returns the source associated with a pid and the app name.
    //
    // If no existing source is found, a new one is created.
    _getSourceForPidAndName(sender, pid, appName) {
        const key = `${pid}${appName}`;
        let source = this._sourceForPidAndName.get(key);

        if (source)
            return source;

        source = new FdoNotificationDaemonSource(sender, null);

        // Only check whether we have a PID since it's enough to identify
        // uniquely an app and "" is a valid app name.
        if (pid) {
            this._sourceForPidAndName.set(key, source);
            source.connect('destroy', () => {
                this._sourceForPidAndName.delete(key);
            });
        }

        Main.messageTray.add(source);
        return source;
    }

    NotifyAsync(params, invocation) {
        let [appName, replacesId, appIcon, summary, body, actions, hints, timeout_] = params;
        let id;

        for (let hint in hints) {
            // unpack the variants
            hints[hint] = hints[hint].deepUnpack();
        }

        hints = {urgency: Urgency.NORMAL, ...hints};

        // Be compatible with the various hints for image data and image path
        // 'image-data' and 'image-path' are the latest name of these hints, introduced in 1.2

        if (!hints['image-path'] && hints['image_path'])
            hints['image-path'] = hints['image_path']; // version 1.1 of the spec

        if (!hints['image-data']) {
            if (hints['image_data'])
                hints['image-data'] = hints['image_data']; // version 1.1 of the spec
            else if (hints['icon_data'] && !hints['image-path'])
                // early versions of the spec; 'icon_data' should only be used if 'image-path' is not available
                hints['image-data'] = hints['icon_data'];
        }

        let source, notification;
        if (replacesId !== 0 && this._notifications.has(replacesId)) {
            notification = this._notifications.get(replacesId);
            source = notification.source;
            id = replacesId;
        } else {
            const sender = hints['x-shell-sender'];
            const pid = hints['x-shell-sender-pid'];
            const appId = hints['desktop-entry'];
            const app = this._getApp(pid, appId, appName);

            id = this._nextNotificationId++;
            source = app
                ? this._getSourceForApp(sender, app)
                : this._getSourceForPidAndName(sender, pid, appName);

            notification = new MessageTray.Notification({source});
            this._notifications.set(id, notification);
            notification.connect('destroy', (n, reason) => {
                this._notifications.delete(id);
                let notificationClosedReason;
                switch (reason) {
                case MessageTray.NotificationDestroyedReason.EXPIRED:
                    notificationClosedReason = NotificationClosedReason.EXPIRED;
                    break;
                case MessageTray.NotificationDestroyedReason.DISMISSED:
                    notificationClosedReason = NotificationClosedReason.DISMISSED;
                    break;
                case MessageTray.NotificationDestroyedReason.SOURCE_CLOSED:
                    notificationClosedReason = NotificationClosedReason.APP_CLOSED;
                    break;
                }
                this._emitNotificationClosed(id, notificationClosedReason);
            });
        }

        const gicon = this._imageForNotificationData(hints);

        const soundFile = 'sound-file' in hints
            ? Gio.File.new_for_path(hints['sound-file']) : null;

        notification.set({
            title: summary,
            body,
            gicon,
            bannerMarkup: true,
            sound: new MessageTray.Sound(soundFile, hints['sound-name']),
            acknowledged: false,
        });
        notification.clearActions();

        let hasDefaultAction = false;

        if (actions.length) {
            for (let i = 0; i < actions.length - 1; i += 2) {
                let [actionId, label] = [actions[i], actions[i + 1]];
                if (actionId === 'default') {
                    hasDefaultAction = true;
                } else {
                    notification.addAction(label, () => {
                        this._emitActivationToken(source, id);
                        this._emitActionInvoked(id, actionId);
                    });
                }
            }
        }

        if (hasDefaultAction) {
            notification.connect('activated', () => {
                this._emitActivationToken(source, id);
                this._emitActionInvoked(id, 'default');
            });
        } else {
            notification.connect('activated', () => {
                source.open();
            });
        }

        switch (hints.urgency) {
        case Urgency.LOW:
            notification.urgency = MessageTray.Urgency.LOW;
            break;
        case Urgency.NORMAL:
            notification.urgency = MessageTray.Urgency.NORMAL;
            break;
        case Urgency.CRITICAL:
            notification.urgency = MessageTray.Urgency.CRITICAL;
            break;
        }
        notification.resident = !!hints.resident;
        // 'transient' is a reserved keyword in JS, so we have to retrieve the value
        // of the 'transient' hint with hints['transient'] rather than hints.transient
        notification.isTransient = !!hints['transient'];

        let privacyScope = hints['x-gnome-privacy-scope'] || 'user';
        notification.privacyScope = privacyScope === 'system'
            ? MessageTray.PrivacyScope.SYSTEM
            : MessageTray.PrivacyScope.USER;

        // Only fallback to 'app-icon' when the source doesn't have a valid app
        const sourceGIcon = source.app ? null : this._iconForNotificationData(appIcon);
        source.processNotification(notification, appName, sourceGIcon);

        return invocation.return_value(GLib.Variant.new('(u)', [id]));
    }

    CloseNotification(id) {
        const notification = this._notifications.get(id);
        notification?.destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
    }

    GetCapabilities() {
        return [
            'actions',
            // 'action-icons',
            'body',
            // 'body-hyperlinks',
            // 'body-images',
            'body-markup',
            // 'icon-multi',
            'icon-static',
            'persistence',
            'sound',
        ];
    }

    GetServerInformation() {
        return [
            Config.PACKAGE_NAME,
            'GNOME',
            Config.PACKAGE_VERSION,
            '1.2',
        ];
    }

    _emitNotificationClosed(id, reason) {
        this._dbusImpl.emit_signal('NotificationClosed',
            GLib.Variant.new('(uu)', [id, reason]));
    }

    _emitActionInvoked(id, action) {
        this._dbusImpl.emit_signal('ActionInvoked',
            GLib.Variant.new('(us)', [id, action]));
    }

    _emitActivationToken(source, id) {
        const context = global.create_app_launch_context(0, -1);
        const info = source.app?.get_app_info();
        if (info) {
            const token = context.get_startup_notify_id(info, []);
            this._dbusImpl.emit_signal('ActivationToken',
                GLib.Variant.new('(us)', [id, token]));
        }
    }
}

export const FdoNotificationDaemonSource = GObject.registerClass(
class FdoNotificationDaemonSource extends MessageTray.Source {
    constructor(sender, app) {
        super({
            policy: MessageTray.NotificationPolicy.newForApp(app),
        });

        this.app = app;
        this._appName = null;
        this._appIcon = null;

        if (sender) {
            this._nameWatcherId = Gio.DBus.session.watch_name(sender,
                Gio.BusNameWatcherFlags.NONE,
                null,
                this._onNameVanished.bind(this));
        } else {
            this._nameWatcherId = 0;
        }
    }

    _onNameVanished() {
        // Destroy the notification source when its sender is removed from DBus.
        // Only do so if this.app is set to avoid removing "notify-send" sources, senders
        // of which аre removed from DBus immediately.
        // Sender being removed from DBus would normally result in a tray icon being removed,
        // so allow the code path that handles the tray icon being removed to handle that case.
        if (this.app)
            this.destroy();
    }

    processNotification(notification, appName, appIcon) {
        if (!this.app && appName) {
            this._appName = appName;
            this.notify('title');
        }

        if (!this.app && appIcon) {
            this._appIcon = appIcon;
            this.notify('icon');
        }

        let tracker = Shell.WindowTracker.get_default();
        // Acknowledge notifications that are resident and their app has the
        // current focus so that we don't show a banner.
        if (notification.resident && this.app && tracker.focus_app === this.app)
            notification.acknowledged = true;

        this.addNotification(notification);
    }

    open() {
        this.openApp();
        this.destroyNonResidentNotifications();
    }

    openApp() {
        if (this.app == null)
            return;

        this.app.activate();
        Main.overview.hide();
        Main.panel.closeCalendar();
    }

    destroy() {
        if (this._nameWatcherId) {
            Gio.DBus.session.unwatch_name(this._nameWatcherId);
            this._nameWatcherId = 0;
        }

        super.destroy();
    }

    get title() {
        return this.app?.get_name() ?? this._appName;
    }

    get icon() {
        return this.app?.get_icon() ?? this._appIcon;
    }
});

const PRIORITY_URGENCY_MAP = {
    low: MessageTray.Urgency.LOW,
    normal: MessageTray.Urgency.NORMAL,
    high: MessageTray.Urgency.HIGH,
    urgent: MessageTray.Urgency.CRITICAL,
};

const GtkNotificationDaemonNotification = GObject.registerClass(
class GtkNotificationDaemonNotification extends MessageTray.Notification {
    constructor(source, id, notification) {
        super({source});
        this._serialized = GLib.Variant.new('a{sv}', notification);
        this.id = id;

        const {
            title,
            body,
            icon: gicon,
            urgent,
            priority,
            buttons,
            'default-action': defaultAction,
            'default-action-target': defaultActionTarget,
            timestamp: time,
        } = notification;

        if (priority) {
            let urgency = PRIORITY_URGENCY_MAP[priority.unpack()];
            this.urgency = urgency !== undefined ? urgency : MessageTray.Urgency.NORMAL;
        } else if (urgent) {
            this.urgency = urgent.unpack()
                ? MessageTray.Urgency.CRITICAL
                : MessageTray.Urgency.NORMAL;
        } else {
            this.urgency = MessageTray.Urgency.NORMAL;
        }

        if (buttons) {
            buttons.deepUnpack().forEach(button => {
                this.addAction(button.label.unpack(), () => {
                    this._onButtonClicked(button);
                });
            });
        }

        this._defaultAction = defaultAction?.unpack();
        this._defaultActionTarget = defaultActionTarget;

        this.set({
            title: title.unpack(),
            body: body?.unpack(),
            gicon: gicon
                ? Gio.icon_deserialize(gicon) : null,
            datetime: time
                ? GLib.DateTime.new_from_unix_local(time.unpack()) : null,
        });
    }

    _activateAction(namespacedActionId, target) {
        if (namespacedActionId) {
            if (namespacedActionId.startsWith('app.')) {
                let actionId = namespacedActionId.slice('app.'.length);
                this.source.activateAction(actionId, target);
            }
        } else {
            this.source.open();
        }
    }

    _onButtonClicked(button) {
        let {action, target} = button;
        this._activateAction(action.unpack(), target);
    }

    activate() {
        this._activateAction(this._defaultAction, this._defaultActionTarget);
        super.activate();
    }

    serialize() {
        return this._serialized;
    }
});

function InvalidAppError() {}

export const GtkNotificationDaemonAppSource = GObject.registerClass(
class GtkNotificationDaemonAppSource extends MessageTray.Source {
    constructor(appId) {
        if (!Gio.Application.id_is_valid(appId))
            throw new InvalidAppError();

        const app = Shell.AppSystem.get_default().lookup_app(`${appId}.desktop`);
        if (!app)
            throw new InvalidAppError();

        super({
            title: app.get_name(),
            icon: app.get_icon(),
            policy: new MessageTray.NotificationApplicationPolicy(appId),
        });

        this._appId = appId;
        this._app = app;

        this._notifications = {};
        this._notificationPending = false;
    }

    activateAction(actionId, target) {
        const params = target ? GLib.Variant.new('av', [target]) : null;
        this._app.activate_action(actionId, params, 0, -1, null).catch(error => {
            logError(error, `Failed to activate action for ${this._appId}`);
        });

        Main.overview.hide();
        Main.panel.closeCalendar();
    }

    open() {
        this._app.activate();
        Main.overview.hide();
        Main.panel.closeCalendar();
    }

    addNotification(notification) {
        this._notificationPending = true;

        this._notifications[notification.id]?.destroy(
            MessageTray.NotificationDestroyedReason.REPLACED);

        notification.connect('destroy', () => {
            delete this._notifications[notification.id];
        });
        this._notifications[notification.id] = notification;

        super.addNotification(notification);

        this._notificationPending = false;
    }

    destroy(reason) {
        if (this._notificationPending)
            return;
        super.destroy(reason);
    }

    removeNotification(notificationId) {
        if (this._notifications[notificationId])
            this._notifications[notificationId].destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
    }

    serialize() {
        let notifications = [];
        for (let notificationId in this._notifications) {
            let notification = this._notifications[notificationId];
            notifications.push([notificationId, notification.serialize()]);
        }
        return [this._appId, notifications];
    }
});

const GtkNotificationsIface = loadInterfaceXML('org.gtk.Notifications');

class GtkNotificationDaemon {
    constructor() {
        this._sources = {};

        this._loadNotifications();

        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GtkNotificationsIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gtk/Notifications');

        Gio.DBus.session.own_name('org.gtk.Notifications', Gio.BusNameOwnerFlags.REPLACE, null, null);
    }

    _ensureAppSource(appId) {
        if (this._sources[appId])
            return this._sources[appId];

        let source = new GtkNotificationDaemonAppSource(appId);

        source.connect('destroy', () => {
            delete this._sources[appId];
            this._saveNotifications();
        });
        source.connect('notify::count', this._saveNotifications.bind(this));
        Main.messageTray.add(source);
        this._sources[appId] = source;
        return source;
    }

    _loadNotifications() {
        this._isLoading = true;

        try {
            let value = global.get_persistent_state('a(sa(sv))', 'notifications');
            if (value) {
                let sources = value.deepUnpack();
                sources.forEach(([appId, notifications]) => {
                    if (notifications.length === 0)
                        return;

                    let source;
                    try {
                        source = this._ensureAppSource(appId);
                    } catch (e) {
                        if (e instanceof InvalidAppError)
                            return;
                        throw e;
                    }

                    notifications.forEach(([notificationId, notificationPacked]) => {
                        const notification = new GtkNotificationDaemonNotification(source,
                            notificationId,
                            notificationPacked.deepUnpack());
                        // Acknowledge all stored notification so that we don't show a banner again
                        notification.acknowledged = true;
                        source.addNotification(notification);
                    });
                });
            }
        } catch (e) {
            logError(e, 'Failed to load saved notifications');
        } finally {
            this._isLoading = false;
        }
    }

    _saveNotifications() {
        if (this._isLoading)
            return;

        let sources = [];
        for (let appId in this._sources) {
            let source = this._sources[appId];
            sources.push(source.serialize());
        }

        global.set_persistent_state('notifications', new GLib.Variant('a(sa(sv))', sources));
    }

    AddNotificationAsync(params, invocation) {
        let [appId, notificationId, notificationSerialized] = params;

        let source;
        try {
            source = this._ensureAppSource(appId);
        } catch (e) {
            if (e instanceof InvalidAppError) {
                invocation.return_error_literal(NotificationErrors,
                    NotificationError.INVALID_APP,
                    `The app by ID "${appId}" could not be found`);
                return;
            }
            throw e;
        }

        let timestamp = GLib.DateTime.new_now_local().to_unix();
        notificationSerialized['timestamp'] = new GLib.Variant('x', timestamp);

        const notification = new GtkNotificationDaemonNotification(source,
            notificationId,
            notificationSerialized);
        source.addNotification(notification);

        invocation.return_value(null);
    }

    RemoveNotificationAsync(params, invocation) {
        let [appId, notificationId] = params;
        let source = this._sources[appId];
        if (source)
            source.removeNotification(notificationId);

        invocation.return_value(null);
    }
}

export class NotificationDaemon {
    constructor() {
        this._fdoNotificationDaemon = new FdoNotificationDaemon();
        this._gtkNotificationDaemon = new GtkNotificationDaemon();
    }
}
(uuay)network.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import NM from 'gi://NM';
import Polkit from 'gi://Polkit';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Config from '../../misc/config.js';
import * as Main from '../main.js';
import * as PopupMenu from '../popupMenu.js';
import * as MessageTray from '../messageTray.js';
import * as ModemManager from '../../misc/modemManager.js';
import * as Signals from '../../misc/signals.js';
import * as Util from '../../misc/util.js';

import {Spinner} from '../animation.js';
import {QuickMenuToggle, SystemIndicator} from '../quickSettings.js';

import {loadInterfaceXML} from '../../misc/fileUtils.js';
import {registerDestroyableType} from '../../misc/signalTracker.js';

Gio._promisify(Gio.DBusConnection.prototype, 'call');
Gio._promisify(NM.Client, 'new_async');
Gio._promisify(NM.Client.prototype, 'check_connectivity_async');
Gio._promisify(NM.DeviceWifi.prototype, 'request_scan_async');

const WIFI_SCAN_FREQUENCY = 15;
const MAX_VISIBLE_NETWORKS = 8;

// small optimization, to avoid using [] all the time
const NM80211Mode = NM['80211Mode'];

/** @enum {number} */
const PortalHelperResult = {
    CANCELLED: 0,
    COMPLETED: 1,
    RECHECK: 2,
};

const PortalHelperIface = loadInterfaceXML('org.gnome.Shell.PortalHelper');
const PortalHelperInfo = Gio.DBusInterfaceInfo.new_for_xml(PortalHelperIface);

function signalToIcon(value) {
    if (value < 20)
        return 'none';
    else if (value < 40)
        return 'weak';
    else if (value < 50)
        return 'ok';
    else if (value < 80)
        return 'good';
    else
        return 'excellent';
}

function ssidToLabel(ssid) {
    let label = NM.utils_ssid_to_utf8(ssid.get_data());
    if (!label)
        label = _('<unknown>');
    return label;
}

function launchSettingsPanel(panel, ...args) {
    const param = new GLib.Variant('(sav)',
        [panel, args.map(s => new GLib.Variant('s', s))]);

    const app = Shell.AppSystem.get_default()
        .lookup_app('org.gnome.Settings.desktop');

    app.activate_action('launch-panel', param, 0, -1, null).catch(error => {
        log(`Failed to launch Settings panel: ${error.message}`);
    });
}

class ItemSorter {
    [Symbol.iterator] = this.items;

    /**
     * Maintains a list of sorted items. By default, items are
     * assumed to be objects with a name property.
     *
     * Optionally items can have a secondary sort order by
     * recency. If used, items must by objects with a timestamp
     * property that can be used in substraction, and "bigger"
     * must mean "more recent". Number and Date both qualify.
     *
     * @param {object=} options - property object with options
     * @param {Function} options.sortFunc - a custom sort function
     * @param {bool} options.trackMru - whether to track MRU order as well
     **/
    constructor(options = {}) {
        const {sortFunc, trackMru} = {
            sortFunc: this._sortByName.bind(this),
            trackMru: false,
            ...options,
        };

        this._trackMru = trackMru;
        this._sortFunc = sortFunc;
        this._sortFuncMru = this._sortByMru.bind(this);

        this._itemsOrder = [];
        this._itemsMruOrder = [];
    }

    *items() {
        yield* this._itemsOrder;
    }

    *itemsByMru() {
        console.assert(this._trackMru, 'itemsByMru: MRU tracking is disabled');
        yield* this._itemsMruOrder;
    }

    _sortByName(one, two) {
        return GLib.utf8_collate(one.name, two.name);
    }

    _sortByMru(one, two) {
        return two.timestamp - one.timestamp;
    }

    _upsert(array, item, sortFunc) {
        this._delete(array, item);
        return Util.insertSorted(array, item, sortFunc);
    }

    _delete(array, item) {
        const pos = array.indexOf(item);
        if (pos >= 0)
            array.splice(pos, 1);
    }

    /**
     * Insert or update item.
     *
     * @param {any} item - the item to upsert
     * @returns {number} - the sorted position of item
     */
    upsert(item) {
        if (this._trackMru)
            this._upsert(this._itemsMruOrder, item, this._sortFuncMru);

        return this._upsert(this._itemsOrder, item, this._sortFunc);
    }

    /**
     * @param {any} item - item to remove
     */
    delete(item) {
        if (this._trackMru)
            this._delete(this._itemsMruOrder, item);
        this._delete(this._itemsOrder, item);
    }
}

const NMMenuItem = GObject.registerClass({
    Properties: {
        'radio-mode': GObject.ParamSpec.boolean('radio-mode', '', '',
            GObject.ParamFlags.READWRITE,
            false),
        'is-active': GObject.ParamSpec.boolean('is-active', '', '',
            GObject.ParamFlags.READABLE,
            false),
        'name': GObject.ParamSpec.string('name', '', '',
            GObject.ParamFlags.READWRITE,
            ''),
        'icon-name': GObject.ParamSpec.string('icon-name', '', '',
            GObject.ParamFlags.READWRITE,
            ''),
    },
}, class NMMenuItem extends PopupMenu.PopupBaseMenuItem {
    get state() {
        return this._activeConnection?.state ??
            NM.ActiveConnectionState.DEACTIVATED;
    }

    get is_active() {
        return this.state <= NM.ActiveConnectionState.ACTIVATED;
    }

    get timestamp() {
        return 0;
    }

    activate() {
        super.activate(Clutter.get_current_event());
    }

    _activeConnectionStateChanged() {
        this.notify('is-active');
        this.notify('icon-name');

        this._sync();
    }

    _setActiveConnection(activeConnection) {
        this._activeConnection?.disconnectObject(this);

        this._activeConnection = activeConnection;

        this._activeConnection?.connectObject(
            'notify::state', () => this._activeConnectionStateChanged(),
            this);
        this._activeConnectionStateChanged();
    }

    _sync() {
        // Overridden by subclasses
    }
});

/**
 * Item that contains a section, and can be collapsed
 * into a submenu
 */
const NMSectionItem = GObject.registerClass({
    Properties: {
        'use-submenu': GObject.ParamSpec.boolean('use-submenu', '', '',
            GObject.ParamFlags.READWRITE,
            false),
    },
}, class NMSectionItem extends NMMenuItem {
    constructor() {
        super({
            activate: false,
            can_focus: false,
        });

        this._useSubmenu = false;

        // Turn into an empty container with no padding
        this.styleClass = '';

        // Add intermediate section; we need this for submenu support
        this._mainSection = new PopupMenu.PopupMenuSection();
        this.add_child(this._mainSection.actor);

        this._submenuItem = new PopupMenu.PopupSubMenuMenuItem('', true);
        this._mainSection.addMenuItem(this._submenuItem);
        this._submenuItem.hide();

        this.section = new PopupMenu.PopupMenuSection();
        this._mainSection.addMenuItem(this.section);

        // Represents the item as a whole when shown
        this.bind_property('name',
            this._submenuItem.label, 'text',
            GObject.BindingFlags.DEFAULT);
        this.bind_property('icon-name',
            this._submenuItem.icon, 'icon-name',
            GObject.BindingFlags.DEFAULT);
    }

    _setParent(parent) {
        super._setParent(parent);
        this._mainSection._setParent(parent);

        parent?.connect('menu-closed',
            () => this._mainSection.emit('menu-closed'));
    }

    get use_submenu() {
        return this._useSubmenu;
    }

    set use_submenu(useSubmenu) {
        if (this._useSubmenu === useSubmenu)
            return;

        this._useSubmenu = useSubmenu;
        this._submenuItem.visible = useSubmenu;

        if (useSubmenu) {
            this._mainSection.box.remove_child(this.section.actor);
            this._submenuItem.menu.box.add_child(this.section.actor);
        } else {
            this._submenuItem.menu.box.remove_child(this.section.actor);
            this._mainSection.box.add_child(this.section.actor);
        }
    }
});

const NMConnectionItem = GObject.registerClass(
class NMConnectionItem extends NMMenuItem {
    constructor(section, connection) {
        super();

        this._section = section;
        this._connection = connection;
        this._activeConnection = null;

        this._icon = new St.Icon({
            style_class: 'popup-menu-icon',
            x_align: Clutter.ActorAlign.END,
            visible: !this.radio_mode,
        });
        this.add_child(this._icon);

        this._label = new St.Label({
            x_expand: true,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this._label);

        this._subtitle = new St.Label({
            style_class: 'device-subtitle',
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this._subtitle);

        this.bind_property('icon-name',
            this._icon, 'icon-name',
            GObject.BindingFlags.DEFAULT);
        this.bind_property('radio-mode',
            this._icon, 'visible',
            GObject.BindingFlags.INVERT_BOOLEAN);

        this.connectObject(
            'notify::radio-mode', () => this._sync(),
            'notify::name', () => this._sync(),
            this);
        this._sync();
    }

    get name() {
        return this._connection.get_id();
    }

    get timestamp() {
        return this._connection.get_setting_connection()?.get_timestamp() ?? 0;
    }

    updateForConnection(connection) {
        // connection should always be the same object
        // (and object path) as this._connection, but
        // this can be false if NetworkManager was restarted
        // and picked up connections in a different order
        // Just to be safe, we set it here again

        this._connection = connection;
        this.notify('name');
        this._sync();
    }

    _getAccessibleName() {
        return this.is_active
            // Translators: %s is a device name like "MyPhone"
            ? _('Disconnect %s').format(this.name)
            // Translators: %s is a device name like "MyPhone"
            : _('Connect to %s').format(this.name);
    }

    _getSubtitleLabel() {
        return this.is_active ? _('Disconnect') : _('Connect');
    }

    _sync() {
        this._label.text = this.name;

        if (this.radioMode) {
            this._subtitle.text = null;
            this.accessible_name = this.name;
            this.accessible_role = Atk.Role.CHECK_MENU_ITEM;
            this.setOrnament(this.is_active
                ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NO_DOT);
        } else {
            this.accessible_name = this._getAccessibleName();
            this._subtitle.text = this._getSubtitleLabel();
            this.accessible_role = Atk.Role.MENU_ITEM;
            this.setOrnament(PopupMenu.Ornament.HIDDEN);
        }
    }

    activate() {
        super.activate();

        if (this.radio_mode && this._activeConnection != null)
            return; // only activate in radio mode

        if (this._activeConnection == null)
            this._section.activateConnection(this._connection);
        else
            this._section.deactivateConnection(this._activeConnection);

        this._sync();
    }

    setActiveConnection(connection) {
        this._setActiveConnection(connection);
    }
});

const NMDeviceConnectionItem = GObject.registerClass({
    Properties: {
        'device-name': GObject.ParamSpec.string('device-name', '', '',
            GObject.ParamFlags.READWRITE,
            ''),
    },
}, class NMDeviceConnectionItem extends NMConnectionItem {
    constructor(section, connection) {
        super(section, connection);

        this.connectObject(
            'notify::radio-mode', () => this.notify('name'),
            'notify::device-name', () => this.notify('name'),
            this);
    }

    get name() {
        return this.radioMode
            ?  this._connection.get_id()
            : this.deviceName;
    }
});

const NMDeviceItem = GObject.registerClass({
    Properties: {
        'single-device-mode': GObject.ParamSpec.boolean('single-device-mode', '', '',
            GObject.ParamFlags.READWRITE,
            false),
    },
}, class NMDeviceItem extends NMSectionItem {
    constructor(client, device) {
        super();

        if (this.constructor === NMDeviceItem)
            throw new TypeError(`Cannot instantiate abstract type ${this.constructor.name}`);

        this._client = client;
        this._device = device;
        this._deviceName = '';

        this._connectionItems = new Map();
        this._itemSorter = new ItemSorter({trackMru: true});

        // Item shown in the 0-connections case
        this._autoConnectItem =
            this.section.addAction(_('Connect'), () => this._autoConnect(), '');

        // Represents the device as a whole when shown
        this.bind_property('name',
            this._autoConnectItem.label, 'text',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('icon-name',
            this._autoConnectItem._icon, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE);

        this._deactivateItem =
            this.section.addAction(_('Turn Off'), () => this.deactivateConnection());

        this._client.connectObject(
            'notify::connectivity', () => this.notify('icon-name'),
            'notify::primary-connection', () => this.notify('icon-name'),
            this);

        this._device.connectObject(
            'notify::available-connections', () => this._syncConnections(),
            'notify::active-connection', () => this._activeConnectionChanged(),
            this);

        this.connect('notify::single-device-mode', () => this._sync());

        this._syncConnections();
        this._activeConnectionChanged();
    }

    get timestamp() {
        const [item] = this._itemSorter.itemsByMru();
        return item?.timestamp ?? 0;
    }

    _canReachInternet() {
        if (this._client.primary_connection !== this._device.active_connection)
            return true;

        return this._client.connectivity === NM.ConnectivityState.FULL;
    }

    _autoConnect() {
        let connection = new NM.SimpleConnection();
        this._client.add_and_activate_connection_async(connection, this._device, null, null, null);
    }

    _activeConnectionChanged() {
        const oldItem = this._connectionItems.get(
            this._activeConnection?.connection);
        oldItem?.setActiveConnection(null);

        this._setActiveConnection(this._device.active_connection);

        const newItem = this._connectionItems.get(
            this._activeConnection?.connection);
        newItem?.setActiveConnection(this._activeConnection);
    }

    _syncConnections() {
        const available = this._device.get_available_connections();
        const removed = [...this._connectionItems.keys()]
            .filter(conn => !available.includes(conn));

        for (const conn of removed)
            this._removeConnection(conn);

        for (const conn of available)
            this._addConnection(conn);
    }

    _getActivatableItem() {
        const [lastUsed] = this._itemSorter.itemsByMru();
        if (lastUsed?.timestamp > 0)
            return lastUsed;

        const [firstItem] = this._itemSorter;
        if (firstItem)
            return firstItem;

        console.assert(this._autoConnectItem.visible,
            `${this}'s autoConnect item should be visible when otherwise empty`);
        return this._autoConnectItem;
    }

    activate() {
        super.activate();

        if (this._activeConnection)
            this.deactivateConnection();
        else
            this._getActivatableItem()?.activate();
    }

    activateConnection(connection) {
        this._client.activate_connection_async(connection, this._device, null, null, null);
    }

    deactivateConnection(_activeConnection) {
        this._device.disconnect(null);
    }

    _onConnectionChanged(connection) {
        const item = this._connectionItems.get(connection);
        item.updateForConnection(connection);
    }

    _resortItem(item) {
        const pos = this._itemSorter.upsert(item);
        this.section.moveMenuItem(item, pos);
    }

    _addConnection(connection) {
        if (this._connectionItems.has(connection))
            return;

        connection.connectObject(
            'changed', this._onConnectionChanged.bind(this),
            this);

        const item = new NMDeviceConnectionItem(this, connection);

        this.bind_property('radio-mode',
            item, 'radio-mode',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('name',
            item, 'device-name',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('icon-name',
            item, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE);
        item.connectObject(
            'notify::name', () => this._resortItem(item),
            this);

        const pos = this._itemSorter.upsert(item);
        this.section.addMenuItem(item, pos);
        this._connectionItems.set(connection, item);
        this._sync();
    }

    _removeConnection(connection) {
        const item = this._connectionItems.get(connection);
        if (!item)
            return;

        this._itemSorter.delete(item);
        this._connectionItems.delete(connection);
        item.destroy();

        this._sync();
    }

    setDeviceName(name) {
        this._deviceName = name;
        this.notify('name');
    }

    _sync() {
        const nItems = this._connectionItems.size;
        this.radio_mode = nItems > 1;
        this.useSubmenu = this.radioMode && !this.singleDeviceMode;
        this._autoConnectItem.visible = nItems === 0;
        this._deactivateItem.visible = this.radioMode && this.isActive;
    }
});

const NMWiredDeviceItem = GObject.registerClass(
class NMWiredDeviceItem extends NMDeviceItem {
    get icon_name() {
        switch (this.state) {
        case NM.ActiveConnectionState.ACTIVATING:
            return 'network-wired-acquiring-symbolic';
        case NM.ActiveConnectionState.ACTIVATED:
            return this._canReachInternet()
                ? 'network-wired-symbolic'
                : 'network-wired-no-route-symbolic';
        default:
            return 'network-wired-disconnected-symbolic';
        }
    }

    get name() {
        return this._deviceName;
    }

    _hasCarrier() {
        if (this._device instanceof NM.DeviceEthernet)
            return this._device.carrier;
        else
            return true;
    }

    _sync() {
        this.visible = this._hasCarrier();
        super._sync();
    }
});

const NMModemDeviceItem = GObject.registerClass(
class NMModemDeviceItem extends NMDeviceItem {
    constructor(client, device) {
        super(client, device);

        this._mobileDevice = null;

        let capabilities = device.current_capabilities;
        if (device.udi.indexOf('/org/freedesktop/ModemManager1/Modem') === 0)
            this._mobileDevice = new ModemManager.BroadbandModem(device.udi, capabilities);
        else if (capabilities & NM.DeviceModemCapabilities.GSM_UMTS)
            this._mobileDevice = new ModemManager.ModemGsm(device.udi);
        else if (capabilities & NM.DeviceModemCapabilities.CDMA_EVDO)
            this._mobileDevice = new ModemManager.ModemCdma(device.udi);
        else if (capabilities & NM.DeviceModemCapabilities.LTE)
            this._mobileDevice = new ModemManager.ModemGsm(device.udi);

        this._mobileDevice?.connectObject(
            'notify::operator-name', this._sync.bind(this),
            'notify::signal-quality', () => this.notify('icon-name'), this);

        Main.sessionMode.connectObject('updated',
            this._sessionUpdated.bind(this), this);
        this._sessionUpdated();
    }

    get icon_name() {
        switch (this.state) {
        case NM.ActiveConnectionState.ACTIVATING:
            return 'network-cellular-acquiring-symbolic';
        case NM.ActiveConnectionState.ACTIVATED: {
            const qualityString = signalToIcon(this._mobileDevice.signal_quality);
            return `network-cellular-signal-${qualityString}-symbolic`;
        }
        default:
            return this._activeConnection
                ? 'network-cellular-signal-none-symbolic'
                : 'network-cellular-disabled-symbolic';
        }
    }

    get name() {
        return this._mobileDevice?.operator_name || this._deviceName;
    }

    get wwanPanelSupported() {
        // Currently, wwan panel doesn't support CDMA_EVDO modems
        const supportedCaps =
            NM.DeviceModemCapabilities.GSM_UMTS |
            NM.DeviceModemCapabilities.LTE;
        return this._device.current_capabilities & supportedCaps;
    }

    _autoConnect() {
        if (this.wwanPanelSupported)
            launchSettingsPanel('wwan', 'show-device', this._device.udi);
        else
            launchSettingsPanel('network', 'connect-3g', this._device.get_path());
    }

    _sessionUpdated() {
        this._autoConnectItem.sensitive = Main.sessionMode.hasWindows;
    }
});

const NMBluetoothDeviceItem = GObject.registerClass(
class NMBluetoothDeviceItem extends NMDeviceItem {
    constructor(client, device) {
        super(client, device);

        this._device.bind_property('name',
            this, 'name',
            GObject.BindingFlags.SYNC_CREATE);
    }

    get icon_name() {
        switch (this.state) {
        case NM.ActiveConnectionState.ACTIVATING:
            return 'network-cellular-acquiring-symbolic';
        case NM.ActiveConnectionState.ACTIVATED:
            return 'network-cellular-connected-symbolic';
        default:
            return this._activeConnection
                ? 'network-cellular-signal-none-symbolic'
                : 'network-cellular-disabled-symbolic';
        }
    }

    get name() {
        return this._device.name;
    }
});

const WirelessNetwork = GObject.registerClass({
    Properties: {
        'name': GObject.ParamSpec.string(
            'name', '', '',
            GObject.ParamFlags.READABLE,
            ''),
        'icon-name': GObject.ParamSpec.string(
            'icon-name', '', '',
            GObject.ParamFlags.READABLE,
            ''),
        'secure': GObject.ParamSpec.boolean(
            'secure', '', '',
            GObject.ParamFlags.READABLE,
            false),
        'is-active': GObject.ParamSpec.boolean(
            'is-active', '', '',
            GObject.ParamFlags.READABLE,
            false),
        'signal-strength': GObject.ParamSpec.uint(
            'signal-strength', '', '',
            GObject.ParamFlags.READABLE,
            0),
    },
    Signals: {
        'destroy': {},
    },
}, class WirelessNetwork extends GObject.Object {
    static _securityTypes =
        Object.values(NM.UtilsSecurityType).sort((a, b) => b - a);

    _init(device) {
        super._init();

        this._device = device;

        this._device.connectObject(
            'notify::active-access-point', () => this.notify('is-active'),
            'notify::active-connection', () => this.notify('is-active'),
            this);

        this._accessPoints = new Set();
        this._connections = [];
        this._name = '';
        this._ssid = null;
        this._bestAp = null;
        this._mode = 0;
        this._securityType = NM.UtilsSecurityType.NONE;
    }

    get signal_strength() {
        return this._bestAp?.strength ?? 0;
    }

    get name() {
        return this._name;
    }

    get icon_name() {
        if (this._mode === NM80211Mode.ADHOC)
            return 'network-workgroup-symbolic';

        if (!this._bestAp)
            return '';

        return `network-wireless-signal-${signalToIcon(this._bestAp.strength)}-symbolic`;
    }

    get secure() {
        return this._securityType !== NM.UtilsSecurityType.NONE &&
            this._securityType !== NM.UtilsSecurityType.UNKOWN &&
            this._securityType !== NM.UtilsSecurityType.OWE &&
            this._securityType !== NM.UtilsSecurityType.OWE_TM;
    }

    get is_active() {
        if (this._accessPoints.has(this._device.activeAccessPoint))
            return true;

        const {activeConnection} = this._device;
        if (activeConnection)
            return this._connections.includes(activeConnection.connection);

        return false;
    }

    hasAccessPoint(ap) {
        return this._accessPoints.has(ap);
    }

    hasAccessPoints() {
        return this._accessPoints.size > 0;
    }

    checkAccessPoint(ap) {
        if (!ap.get_ssid())
            return false;

        const secType = this._getApSecurityType(ap);
        if (secType === NM.UtilsSecurityType.INVALID)
            return false;

        if (this._accessPoints.size === 0)
            return true;

        return this._ssid.equal(ap.ssid) &&
            this._mode === ap.mode &&
            this._securityType === secType;
    }

    /**
     * @param {NM.AccessPoint} ap - an access point
     * @returns {bool} - whether the access point was added
     */
    addAccessPoint(ap) {
        if (!this.checkAccessPoint(ap))
            return false;

        if (this._accessPoints.size === 0) {
            this._ssid = ap.get_ssid();
            this._mode = ap.mode;
            this._securityType = this._getApSecurityType(ap);
            this._name = NM.utils_ssid_to_utf8(this._ssid.get_data()) || '<unknown>';

            this.notify('name');
            this.notify('secure');
        }

        const wasActive = this.is_active;
        this._accessPoints.add(ap);

        ap.connectObject(
            'notify::strength', () => {
                this.notify('icon-name');
                this.notify('signal-strength');
                this._updateBestAp();
            }, this);
        this._updateBestAp();

        if (wasActive !== this.is_active)
            this.notify('is-active');

        return true;
    }

    /**
     * @param {NM.AccessPoint} ap - an access point
     * @returns {bool} - whether the access point was removed
     */
    removeAccessPoint(ap) {
        const wasActive = this.is_active;
        if (!this._accessPoints.delete(ap))
            return false;

        ap.disconnectObject(this);
        this._updateBestAp();

        if (wasActive !== this.is_active)
            this.notify('is-active');

        return true;
    }

    /**
     * @param {WirelessNetwork} other - network to compare with
     * @returns {number} - the sort order
     */
    compare(other) {
        // place known connections first
        const cmpConnections = other.hasConnections() - this.hasConnections();
        if (cmpConnections !== 0)
            return cmpConnections;

        const cmpAps = other.hasAccessPoints() - this.hasAccessPoints();
        if (cmpAps !== 0)
            return cmpAps;

        // place stronger connections first
        const cmpStrength = other.signal_strength - this.signal_strength;
        if (cmpStrength !== 0)
            return cmpStrength;

        // place secure connections first
        const cmpSec = other.secure - this.secure;
        if (cmpSec !== 0)
            return cmpSec;

        // sort alphabetically
        return GLib.utf8_collate(this._name, other._name);
    }

    hasConnections() {
        return this._connections.length > 0;
    }

    checkConnections(connections) {
        const aps = [...this._accessPoints];
        this._connections = connections.filter(
            c => aps.some(ap => ap.connection_valid(c)));
    }

    canAutoconnect() {
        const canAutoconnect =
            this._securityTypes !== NM.UtilsSecurityType.WPA_ENTERPRISE &&
            this._securityTypes !== NM.UtilsSecurityType.WPA2_ENTERPRISE;
        return canAutoconnect;
    }

    activate() {
        const [ap] = this._accessPoints;
        let [conn] = this._connections;
        if (conn) {
            this._device.client.activate_connection_async(conn, this._device, null, null, null);
        } else if (!this.canAutoconnect()) {
            launchSettingsPanel('wifi', 'connect-8021x-wifi',
                this._getDeviceDBusPath(), ap.get_path());
        } else {
            conn = new NM.SimpleConnection();
            this._device.client.add_and_activate_connection_async(
                conn, this._device, ap.get_path(), null, null);
        }
    }

    destroy() {
        this.emit('destroy');
    }

    _getDeviceDBusPath() {
        // nm_object_get_path() is shadowed by nm_device_get_path()
        return NM.Object.prototype.get_path.call(this._device);
    }

    _getApSecurityType(ap) {
        const {wirelessCapabilities: caps} = this._device;
        const {flags, wpaFlags, rsnFlags} = ap;
        const haveAp = true;
        const adHoc = ap.mode === NM80211Mode.ADHOC;
        const bestType = WirelessNetwork._securityTypes
            .find(t => NM.utils_security_valid(t, caps, haveAp, adHoc, flags, wpaFlags, rsnFlags));
        return bestType ?? NM.UtilsSecurityType.INVALID;
    }

    _updateBestAp() {
        const [bestAp] =
            [...this._accessPoints].sort((a, b) => b.strength - a.strength);

        if (this._bestAp === bestAp)
            return;

        this._bestAp = bestAp;
        this.notify('icon-name');
    }
});
registerDestroyableType(WirelessNetwork);

const NMWirelessNetworkItem = GObject.registerClass(
class NMWirelessNetworkItem extends PopupMenu.PopupBaseMenuItem {
    _init(network) {
        super._init({style_class: 'nm-network-item'});

        this._network = network;

        const icons = new St.BoxLayout();
        this.add_child(icons);

        this._signalIcon = new St.Icon({style_class: 'popup-menu-icon'});
        icons.add_child(this._signalIcon);

        this._secureIcon = new St.Icon({
            style_class: 'wireless-secure-icon',
            y_align: Clutter.ActorAlign.END,
        });
        icons.add_child(this._secureIcon);

        this._label = new St.Label();
        this.add_child(this._label);

        this._selectedIcon = new St.Icon({
            style_class: 'popup-menu-icon',
            icon_name: 'object-select-symbolic',
        });
        this.add_child(this._selectedIcon);

        this._network.bind_property('icon-name',
            this._signalIcon, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE);
        this._network.bind_property('name',
            this._label, 'text',
            GObject.BindingFlags.SYNC_CREATE);
        this._network.bind_property('is-active',
            this._selectedIcon, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        this._network.bind_property_full('secure',
            this._secureIcon, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE,
            (bind, source) => [true, source ? 'network-wireless-encrypted-symbolic' : ''],
            null);
        this._network.connectObject(
            'notify::is-active', () => this._isActiveChanged(),
            'notify::secure', () => this._updateAccessibleName(),
            'notify::signal-strength', () => this._updateAccessibleName(),
            this);
    }

    get network() {
        return this._network;
    }

    _isActiveChanged() {
        if (this._network.is_active)
            this.add_accessible_state(Atk.StateType.CHECKED);
        else
            this.remove_accessible_state(Atk.StateType.CHECKED);
    }

    _updateAccessibleName() {
        const secureString = this._network.secure ? _('Secure') : _('Not secure');
        let signalStrengthString = _('Signal strength %s%%').format(this._network.signal_strength);
        // translators: The first placeholder is the network name, the second and indication whether it is secure, and the last the signal strength indication
        this.accessible_name = _('%s, %s, %s').format(this._label.text, secureString, signalStrengthString);
    }
});

const NMWirelessDeviceItem = GObject.registerClass({
    Properties: {
        'is-hotspot': GObject.ParamSpec.boolean('is-hotspot', '', '',
            GObject.ParamFlags.READABLE,
            false),
        'single-device-mode': GObject.ParamSpec.boolean('single-device-mode', '', '',
            GObject.ParamFlags.READWRITE,
            false),
    },
}, class NMWirelessDeviceItem extends NMSectionItem {
    constructor(client, device) {
        super();

        this._client = client;
        this._device = device;

        this._deviceName = '';

        this._networkItems = new Map();
        this._itemSorter = new ItemSorter({
            sortFunc: (one, two) => one.network.compare(two.network),
        });

        this._client.connectObject(
            'notify::wireless-enabled', () => this.notify('icon-name'),
            'notify::connectivity', () => this.notify('icon-name'),
            'notify::primary-connection', () => this.notify('icon-name'),
            this);

        this._device.connectObject(
            'notify::active-access-point', this._activeApChanged.bind(this),
            'notify::active-connection', () => this._activeConnectionChanged(),
            'notify::available-connections', () => this._availableConnectionsChanged(),
            'state-changed', () => this.notify('is-hotspot'),
            'access-point-added', (d, ap) => {
                this._addAccessPoint(ap);
                this._updateItemsVisibility();
            },
            'access-point-removed', (d, ap) => {
                this._removeAccessPoint(ap);
                this._updateItemsVisibility();
            }, this);

        this.bind_property('single-device-mode',
            this, 'use-submenu',
            GObject.BindingFlags.INVERT_BOOLEAN);

        Main.sessionMode.connectObject('updated',
            () => this._updateItemsVisibility(),
            this);

        for (const ap of this._device.get_access_points())
            this._addAccessPoint(ap);

        this._activeApChanged();
        this._activeConnectionChanged();
        this._availableConnectionsChanged();
        this._updateItemsVisibility();

        this.connect('destroy', () => {
            for (const net of this._networkItems.keys())
                net.destroy();
        });
    }

    get icon_name() {
        if (!this._device.client.wireless_enabled)
            return 'network-wireless-disabled-symbolic';

        switch (this.state) {
        case NM.ActiveConnectionState.ACTIVATING:
            return 'network-wireless-acquiring-symbolic';

        case NM.ActiveConnectionState.ACTIVATED: {
            if (this.is_hotspot)
                return 'network-wireless-hotspot-symbolic';

            if (!this._canReachInternet())
                return 'network-wireless-no-route-symbolic';

            if (!this._activeAccessPoint) {
                if (this._device.mode !== NM80211Mode.ADHOC)
                    console.info('An active wireless connection, in infrastructure mode, involves no access point?');

                return 'network-wireless-connected-symbolic';
            }

            const {strength} = this._activeAccessPoint;
            return `network-wireless-signal-${signalToIcon(strength)}-symbolic`;
        }
        default:
            return 'network-wireless-signal-none-symbolic';
        }
    }

    get name() {
        if (this.is_hotspot)
            /* Translators: %s is a network identifier */
            return _('%s Hotspot').format(this._deviceName);

        const {ssid} = this._activeAccessPoint ?? {};
        if (ssid)
            return ssidToLabel(ssid);

        // Use connection name when connected to hidden AP
        const {activeConnection} = this._device;
        if (activeConnection)
            return activeConnection.connection.get_id();

        return this._deviceName;
    }

    get is_hotspot() {
        if (!this._device.active_connection)
            return false;

        const {connection} = this._device.active_connection;
        if (!connection)
            return false;

        let ip4config = connection.get_setting_ip4_config();
        if (!ip4config)
            return false;

        return ip4config.get_method() === NM.SETTING_IP4_CONFIG_METHOD_SHARED;
    }

    activate() {
        if (!this.is_hotspot)
            return;

        const {activeConnection} = this._device;
        this._client.deactivate_connection_async(activeConnection, null, null);
    }

    _activeApChanged() {
        this._activeAccessPoint?.disconnectObject(this);
        this._activeAccessPoint = this._device.active_access_point;
        this._activeAccessPoint?.connectObject(
            'notify::strength', () => this.notify('icon-name'),
            'notify::ssid', () => this.notify('name'),
            this);

        this.notify('icon-name');
        this.notify('name');
    }

    _activeConnectionChanged() {
        this._setActiveConnection(this._device.active_connection);
    }

    _availableConnectionsChanged() {
        const connections = this._device.get_available_connections();
        for (const net of this._networkItems.keys())
            net.checkConnections(connections);
    }

    _addAccessPoint(ap) {
        if (ap.get_ssid() == null) {
            // This access point is not visible yet
            // Wait for it to get a ssid
            ap.connectObject('notify::ssid', () => {
                if (!ap.ssid)
                    return;
                ap.disconnectObject(this);
                this._addAccessPoint(ap);
            }, this);
            return;
        }

        let network = [...this._networkItems.keys()]
            .find(n => n.checkAccessPoint(ap));

        if (!network) {
            network = new WirelessNetwork(this._device);

            const item = new NMWirelessNetworkItem(network);
            item.connect('activate', () => network.activate());

            network.connectObject(
                'notify::icon-name', () => this._resortItem(item),
                'notify::is-active', () => this._resortItem(item),
                this);

            const pos = this._itemSorter.upsert(item);
            this.section.addMenuItem(item, pos);
            this._networkItems.set(network, item);
        }

        network.addAccessPoint(ap);
    }

    _removeAccessPoint(ap) {
        const network = [...this._networkItems.keys()]
            .find(n => n.removeAccessPoint(ap));

        if (!network || network.hasAccessPoints())
            return;

        const item = this._networkItems.get(network);
        this._itemSorter.delete(item);
        this._networkItems.delete(network);

        item?.destroy();
        network.destroy();
    }

    _resortItem(item) {
        const pos = this._itemSorter.upsert(item);
        this.section.moveMenuItem(item, pos);

        this._updateItemsVisibility();
    }

    _updateItemsVisibility() {
        const {hasWindows} = Main.sessionMode;

        let nVisible = 0;
        for (const item of this._itemSorter) {
            const {network: net} = item;
            item.visible =
                (hasWindows || net.hasConnections() || net.canAutoconnect()) &&
                nVisible < MAX_VISIBLE_NETWORKS;
            if (item.visible)
                nVisible++;
        }
    }

    setDeviceName(name) {
        this._deviceName = name;
        this.notify('name');
    }

    _canReachInternet() {
        if (this._client.primary_connection !== this._device.active_connection)
            return true;

        return this._client.connectivity === NM.ConnectivityState.FULL;
    }
});

const NMVpnConnectionItem = GObject.registerClass({
    Signals: {
        'activation-failed': {},
    },
}, class NMVpnConnectionItem extends NMConnectionItem {
    constructor(section, connection) {
        super(section, connection);

        this._label.x_expand = true;
        this.accessible_role = Atk.Role.CHECK_MENU_ITEM;
        this._icon.hide();
        this.label_actor = this._label;

        this._switch = new PopupMenu.Switch(this.is_active);
        this.add_child(this._switch);

        this.bind_property('is-active',
            this._switch, 'state',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('name',
            this._label, 'text',
            GObject.BindingFlags.SYNC_CREATE);
    }

    _sync() {
        if (this.is_active)
            this.add_accessible_state(Atk.StateType.CHECKED);
        else
            this.remove_accessible_state(Atk.StateType.CHECKED);
    }

    _activeConnectionStateChanged() {
        const state = this._activeConnection?.get_state();
        const reason = this._activeConnection?.get_state_reason();

        if (state === NM.ActiveConnectionState.DEACTIVATED &&
            reason !== NM.ActiveConnectionStateReason.NO_SECRETS &&
            reason !== NM.ActiveConnectionStateReason.USER_DISCONNECTED)
            this.emit('activation-failed');

        super._activeConnectionStateChanged();
    }

    get icon_name() {
        switch (this.state) {
        case NM.ActiveConnectionState.ACTIVATING:
            return 'network-vpn-acquiring-symbolic';
        case NM.ActiveConnectionState.ACTIVATED:
            return 'network-vpn-symbolic';
        default:
            return 'network-vpn-disabled-symbolic';
        }
    }

    set icon_name(_ignored) {
    }
});

const NMToggle = GObject.registerClass({
    Signals: {
        'activation-failed': {},
    },
}, class NMToggle extends QuickMenuToggle {
    constructor() {
        super();

        this._items = new Map();
        this._itemSorter = new ItemSorter({trackMru: true});

        this._itemsSection = new PopupMenu.PopupMenuSection();
        this.menu.addMenuItem(this._itemsSection);

        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._itemBinding = new GObject.BindingGroup();
        this._itemBinding.bind('icon-name',
            this, 'icon-name', GObject.BindingFlags.DEFAULT);
        this._itemBinding.bind_property_full('source',
            this, 'title', GObject.BindingFlags.DEFAULT,
            () => [true, this._getDefaultName()],
            null);
        this._itemBinding.bind_full('name',
            this, 'subtitle', GObject.BindingFlags.DEFAULT,
            (bind, source) => [true, this._transformSubtitle(source)],
            null);

        this.connect('clicked', () => this.activate());
    }

    setClient(client) {
        if (this._client === client)
            return;

        this._client?.disconnectObject(this);
        this._client = client;
        this._client?.connectObject(
            'notify::networking-enabled', () => this._sync(),
            this);

        this._items.forEach(item => item.destroy());
        this._items.clear();

        if (this._client)
            this._loadInitialItems();
        this._sync();
    }

    activate() {
        const activeItems = [...this._getActiveItems()];

        if (activeItems.length > 0)
            activeItems.forEach(i => i.activate());
        else
            this._itemBinding.source?.activate();
    }

    _loadInitialItems() {
        throw new GObject.NotImplementedError();
    }

    // transform function for property binding:
    // Ignore the provided label if there are multiple active
    // items, and replace it with something like "2 connected"
    _transformSubtitle(source) {
        const nActive = this.checked
            ? [...this._getActiveItems()].length
            : 0;
        if (nActive > 1)
            return ngettext('%d connected', '%d connected', nActive).format(nActive);
        return source;
    }

    _updateItemsVisibility() {
        [...this._itemSorter.itemsByMru()].forEach(
            (item, i) => (item.visible = i < MAX_VISIBLE_NETWORKS));
    }

    _itemActiveChanged() {
        // force an update in case we changed
        // from or to multiple active items
        this._itemBinding.source?.notify('name');
        this._sync();
    }

    _updateChecked() {
        const [firstActive] = this._getActiveItems();
        this.checked = !!firstActive;
    }

    _resortItem(item) {
        const pos = this._itemSorter.upsert(item);
        this._itemsSection.moveMenuItem(item, pos);
    }

    _addItem(key, item) {
        console.assert(!this._items.has(key),
            `${this} already has an item for ${key}`);

        item.connectObject(
            'notify::is-active', () => this._itemActiveChanged(),
            'notify::name', () => this._resortItem(item),
            'destroy', () => this._removeItem(key),
            this);

        this._items.set(key, item);
        const pos = this._itemSorter.upsert(item);
        this._itemsSection.addMenuItem(item, pos);
        this._sync();
    }

    _removeItem(key) {
        const item = this._items.get(key);
        if (!item)
            return;

        this._itemSorter.delete(item);
        this._items.delete(key);

        item.destroy();
        this._sync();
    }

    *_getActiveItems() {
        for (const item of this._itemSorter) {
            if (item.is_active)
                yield item;
        }
    }

    _getPrimaryItem() {
        // prefer active items
        const [firstActive] = this._getActiveItems();
        if (firstActive)
            return firstActive;

        // otherwise prefer the most-recently used
        const [lastUsed] = this._itemSorter.itemsByMru();
        if (lastUsed?.timestamp > 0)
            return lastUsed;

        // as a last resort, return the top-most visible item
        for (const item of this._itemSorter) {
            if (item.visible)
                return item;
        }

        console.assert(!this.visible,
            `${this} should not be visible when empty`);

        return null;
    }

    _sync() {
        this.visible =
            this._client?.networking_enabled && this._items.size > 0;
        this._updateItemsVisibility();
        this._updateChecked();
        this._itemBinding.source = this._getPrimaryItem();
    }
});

const NMVpnToggle = GObject.registerClass(
class NMVpnToggle extends NMToggle {
    constructor() {
        super();

        this.menu.setHeader('network-vpn-symbolic', _('VPN'));
        this.menu.addSettingsAction(_('VPN Settings'),
            'gnome-network-panel.desktop');
    }

    setClient(client) {
        super.setClient(client);

        this._client?.connectObject(
            'connection-added', (c, conn) => this._addConnection(conn),
            'connection-removed', (c, conn) => this._removeConnection(conn),
            'notify::active-connections', () => this._syncActiveConnections(),
            this);
    }

    _getDefaultName() {
        return _('VPN');
    }

    _loadInitialItems() {
        const connections = this._client.get_connections();
        for (const conn of connections)
            this._addConnection(conn);

        this._syncActiveConnections();
    }

    _syncActiveConnections() {
        const activeConnections =
            this._client.get_active_connections().filter(
                c => this._shouldHandleConnection(c.connection));

        for (const item of this._items.values())
            item.setActiveConnection(null);

        for (const a of activeConnections)
            this._items.get(a.connection)?.setActiveConnection(a);
    }

    _shouldHandleConnection(connection) {
        const setting = connection.get_setting_connection();
        if (!setting)
            return false;

        // Ignore slave connection
        if (setting.get_master())
            return false;

        const handledTypes = [
            NM.SETTING_VPN_SETTING_NAME,
            NM.SETTING_WIREGUARD_SETTING_NAME,
        ];
        return handledTypes.includes(setting.type);
    }

    _onConnectionChanged(connection) {
        const item = this._items.get(connection);
        item.updateForConnection(connection);
    }

    _addConnection(connection) {
        if (this._items.has(connection))
            return;

        if (!this._shouldHandleConnection(connection))
            return;

        connection.connectObject(
            'changed', this._onConnectionChanged.bind(this),
            this);

        const item = new NMVpnConnectionItem(this, connection);
        item.connectObject(
            'activation-failed', () => this.emit('activation-failed'),
            this);
        this._addItem(connection, item);

        // FIXME: NM is emitting "connection-added" after "notify::active-connections",
        // so we need to sync connections here once again.
        this._syncActiveConnections();
    }

    _removeConnection(connection) {
        this._removeItem(connection);
    }

    activateConnection(connection) {
        this._client.activate_connection_async(connection, null, null, null, null);
    }

    deactivateConnection(activeConnection) {
        this._client.deactivate_connection(activeConnection, null);
    }
});

const NMDeviceToggle = GObject.registerClass(
class NMDeviceToggle extends NMToggle {
    constructor(deviceType) {
        super();

        this._deviceType = deviceType;
        this._nmDevices = new Set();
        this._deviceNames = new Map();
    }

    setClient(client) {
        this._nmDevices.clear();

        super.setClient(client);

        this._client?.connectObject(
            'device-added', (c, dev) => {
                this._addDevice(dev);
                this._syncDeviceNames();
            },
            'device-removed', (c, dev) => {
                this._removeDevice(dev);
                this._syncDeviceNames();
            }, this);
    }

    _getDefaultName() {
        const [dev] = this._nmDevices;
        const [name] = NM.Device.disambiguate_names([dev]);
        return name;
    }

    _transformSubtitle(source) {
        const subtitle = super._transformSubtitle(source);
        if (subtitle === this.title)
            return null;
        return subtitle;
    }

    _loadInitialItems() {
        const devices = this._client.get_devices();
        for (const  dev of devices)
            this._addDevice(dev);
        this._syncDeviceNames();
    }

    _shouldShowDevice(device) {
        switch (device.state) {
        case NM.DeviceState.DISCONNECTED:
        case NM.DeviceState.ACTIVATED:
        case NM.DeviceState.DEACTIVATING:
        case NM.DeviceState.PREPARE:
        case NM.DeviceState.CONFIG:
        case NM.DeviceState.IP_CONFIG:
        case NM.DeviceState.IP_CHECK:
        case NM.DeviceState.SECONDARIES:
        case NM.DeviceState.NEED_AUTH:
        case NM.DeviceState.FAILED:
            return true;
        case NM.DeviceState.UNMANAGED:
        case NM.DeviceState.UNAVAILABLE:
        default:
            return false;
        }
    }

    _syncDeviceNames() {
        const devices = [...this._nmDevices];
        const names = NM.Device.disambiguate_names(devices);
        this._deviceNames.clear();
        devices.forEach(
            (dev, i) => {
                this._deviceNames.set(dev, names[i]);
                this._items.get(dev)?.setDeviceName(names[i]);
            });
    }

    _syncDeviceItem(device) {
        if (this._shouldShowDevice(device))
            this._ensureDeviceItem(device);
        else
            this._removeDeviceItem(device);
    }

    _deviceStateChanged(device, newState, oldState, reason) {
        if (newState === oldState) {
            console.info(`${device} emitted state-changed without actually changing state`);
            return;
        }

        /* Emit a notification if activation fails, but don't do it
           if the reason is no secrets, as that indicates the user
           cancelled the agent dialog */
        if (newState === NM.DeviceState.FAILED &&
            reason !== NM.DeviceStateReason.NO_SECRETS)
            this.emit('activation-failed');
    }

    _createDeviceMenuItem(_device) {
        throw new GObject.NotImplementedError();
    }

    _ensureDeviceItem(device) {
        if (this._items.has(device))
            return;

        const item = this._createDeviceMenuItem(device);
        item.setDeviceName(this._deviceNames.get(device) ?? '');
        this._addItem(device, item);
    }

    _removeDeviceItem(device) {
        this._removeItem(device);
    }

    _addDevice(device) {
        if (this._nmDevices.has(device))
            return;

        if (device.get_device_type() !== this._deviceType)
            return;

        device.connectObject(
            'state-changed', this._deviceStateChanged.bind(this),
            'notify::interface', () => this._syncDeviceNames(),
            'notify::state', () => this._syncDeviceItem(device),
            this);

        this._nmDevices.add(device);
        this._syncDeviceItem(device);
    }

    _removeDevice(device) {
        if (!this._nmDevices.delete(device))
            return;

        device.disconnectObject(this);
        this._removeDeviceItem(device);
    }

    _sync() {
        super._sync();

        const nItems = this._items.size;
        this._items.forEach(item => (item.singleDeviceMode = nItems === 1));
    }
});

const NMWirelessToggle = GObject.registerClass(
class NMWirelessToggle extends NMDeviceToggle {
    constructor() {
        super(NM.DeviceType.WIFI);

        this._itemBinding.bind('is-hotspot',
            this, 'menu-enabled',
            GObject.BindingFlags.INVERT_BOOLEAN);

        this._scanningSpinner = new Spinner(16);

        this.menu.connectObject('open-state-changed', (m, isOpen) => {
            if (isOpen)
                this._startScanning();
            else
                this._stopScanning();
        });

        this.menu.setHeader('network-wireless-symbolic', _('Wi–Fi'));
        this.menu.addHeaderSuffix(this._scanningSpinner);
        this.menu.addSettingsAction(_('All Networks'),
            'gnome-wifi-panel.desktop');
    }

    setClient(client) {
        super.setClient(client);

        this._client?.bind_property('wireless-enabled',
            this, 'checked',
            GObject.BindingFlags.SYNC_CREATE);
        this._client?.bind_property('wireless-hardware-enabled',
            this, 'reactive',
            GObject.BindingFlags.SYNC_CREATE);
    }

    activate() {
        const primaryItem = this._itemBinding.source;
        if (primaryItem?.is_hotspot)
            primaryItem.activate();
        else
            this._client.wireless_enabled = !this._client.wireless_enabled;
    }

    async _scanDevice(device) {
        const {lastScan} = device;
        await device.request_scan_async(null);

        // Wait for the lastScan property to update, which
        // indicates the end of the scan
        return new Promise(resolve => {
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1500, () => {
                if (device.lastScan === lastScan)
                    return GLib.SOURCE_CONTINUE;

                resolve();
                return GLib.SOURCE_REMOVE;
            });
        });
    }

    async _scanDevices() {
        if (!this._client.wireless_enabled)
            return;

        this._scanningSpinner.play();

        const devices = [...this._items.keys()];
        await Promise.all(
            devices.map(d => this._scanDevice(d)));

        this._scanningSpinner.stop();
    }

    _startScanning() {
        this._scanTimeoutId = GLib.timeout_add_seconds(
            GLib.PRIORITY_DEFAULT, WIFI_SCAN_FREQUENCY, () => {
                this._scanDevices().catch(logError);
                return GLib.SOURCE_CONTINUE;
            });
        this._scanDevices().catch(logError);
    }

    _stopScanning() {
        if (this._scanTimeoutId)
            GLib.source_remove(this._scanTimeoutId);
        delete this._scanTimeoutId;
    }

    _createDeviceMenuItem(device) {
        return new NMWirelessDeviceItem(this._client, device);
    }

    _updateChecked() {
        // handled via a property binding
    }

    _getPrimaryItem() {
        const hotspot = [...this._items.values()].find(i => i.is_hotspot);
        if (hotspot)
            return hotspot;

        return super._getPrimaryItem();
    }

    _shouldShowDevice(device) {
        // don't disappear if wireless-enabled is false
        if (device.state === NM.DeviceState.UNAVAILABLE)
            return true;
        return super._shouldShowDevice(device);
    }
});

const NMWiredToggle = GObject.registerClass(
class NMWiredToggle extends NMDeviceToggle {
    constructor() {
        super(NM.DeviceType.ETHERNET);

        this.menu.setHeader('network-wired-symbolic', _('Wired Connections'));
        this.menu.addSettingsAction(_('Wired Settings'),
            'gnome-network-panel.desktop');
    }

    _createDeviceMenuItem(device) {
        return new NMWiredDeviceItem(this._client, device);
    }
});

const NMBluetoothToggle = GObject.registerClass(
class NMBluetoothToggle extends NMDeviceToggle {
    constructor() {
        super(NM.DeviceType.BT);

        this.menu.setHeader('network-cellular-symbolic', _('Bluetooth Tethers'));
        this.menu.addSettingsAction(_('Bluetooth Settings'),
            'gnome-network-panel.desktop');
    }

    _getDefaultName() {
        // Translators: "Tether" from "Bluetooth Tether"
        return _('Tether');
    }

    _createDeviceMenuItem(device) {
        return new NMBluetoothDeviceItem(this._client, device);
    }
});

const NMModemToggle = GObject.registerClass(
class NMModemToggle extends NMDeviceToggle {
    constructor() {
        super(NM.DeviceType.MODEM);

        this.menu.setHeader('network-cellular-symbolic', _('Mobile Connections'));

        const settingsLabel = _('Mobile Broadband Settings');
        this._wwanSettings = this.menu.addSettingsAction(settingsLabel,
            'gnome-wwan-panel.desktop');
        this._legacySettings = this.menu.addSettingsAction(settingsLabel,
            'gnome-network-panel.desktop');
    }

    _getDefaultName() {
        // Translators: "Mobile" from "Mobile Broadband"
        return _('Mobile');
    }

    _createDeviceMenuItem(device) {
        return new NMModemDeviceItem(this._client, device);
    }

    _sync() {
        super._sync();

        const useWwanPanel =
            [...this._items.values()].some(i => i.wwanPanelSupported);
        this._wwanSettings.visible = useWwanPanel;
        this._legacySettings.visible = !useWwanPanel;
    }
});

class CaptivePortalHandler extends Signals.EventEmitter {
    constructor(checkUri) {
        super();

        this._checkUri = checkUri;
        this._connectivityQueue = new Set();
        this._notifications = new Map();
        this._portalHelperProxy = null;
    }

    addConnection(name, path) {
        if (this._connectivityQueue.has(path) || this._notifications.has(path))
            return;

        const source = MessageTray.getSystemSource();

        const notification = new MessageTray.Notification({
            title: _('Sign Into Wi–Fi Network'),
            body: name,
            source,
        });
        notification.connect('activated',
            () => this._onNotificationActivated(path));
        notification.connect('destroy',
            () => this._notifications.delete(path));
        this._notifications.set(path, notification);
        source.addNotification(notification);
    }


    removeConnection(path) {
        if (this._connectivityQueue.delete(path))
            this._portalHelperProxy?.CloseAsync(path);
        this._notifications.get(path)?.destroy(
            MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
        this._notifications.delete(path);
    }

    _onNotificationActivated(path) {
        const context = global.create_app_launch_context(
            global.get_current_time(), -1);

        if (Config.HAVE_PORTAL_HELPER)
            this._launchPortalHelper(path, context).catch(logError);
        else
            Gio.AppInfo.launch_default_for_uri(this._checkUri, context);

        Main.overview.hide();
        Main.panel.closeCalendar();
    }

    _portalHelperDone(parameters) {
        const [path, result] = parameters;

        if (result === PortalHelperResult.CANCELLED) {
            // Keep the connection in the queue, so the user is not
            // spammed with more logins until we next flush the queue,
            // which will happen once they choose a better connection
            // or we get to full connectivity through other means
        } else if (result === PortalHelperResult.COMPLETED) {
            this.removeConnection(path);
        } else if (result === PortalHelperResult.RECHECK) {
            this.emit('recheck', path);
        } else {
            log(`Invalid result from portal helper: ${result}`);
        }
    }

    async _launchPortalHelper(path, context) {
        if (!this._portalHelperProxy) {
            this._portalHelperProxy = new Gio.DBusProxy({
                g_connection: Gio.DBus.session,
                g_name: 'org.gnome.Shell.PortalHelper',
                g_object_path: '/org/gnome/Shell/PortalHelper',
                g_interface_name: PortalHelperInfo.name,
                g_interface_info: PortalHelperInfo,
            });
            this._portalHelperProxy.connectSignal('Done',
                (proxy, emitter, params) => {
                    this._portalHelperDone(params);
                });

            try {
                await this._portalHelperProxy.init_async(
                    GLib.PRIORITY_DEFAULT, null);
            } catch (e) {
                console.error(`Error launching the portal helper: ${e.message}`);
            }
        }

        const {timestamp} = context;
        this._portalHelperProxy?.AuthenticateAsync(path, this._checkUri, timestamp).catch(logError);
        this._connectivityQueue.add(path);
    }

    clear() {
        for (const item of this._connectivityQueue)
            this._portalHelperProxy?.CloseAsync(item);
        this._connectivityQueue.clear();

        for (const n of this._notifications.values())
            n.destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
        this._notifications.clear();
    }
}

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this._portalHandler = null;

        this._mainConnection = null;

        this._notification = null;

        this._wiredToggle = new NMWiredToggle();
        this._wirelessToggle = new NMWirelessToggle();
        this._modemToggle = new NMModemToggle();
        this._btToggle = new NMBluetoothToggle();
        this._vpnToggle = new NMVpnToggle();

        this._deviceToggles = new Map([
            [NM.DeviceType.ETHERNET, this._wiredToggle],
            [NM.DeviceType.WIFI, this._wirelessToggle],
            [NM.DeviceType.MODEM, this._modemToggle],
            [NM.DeviceType.BT, this._btToggle],
        ]);
        this.quickSettingsItems.push(...this._deviceToggles.values());
        this.quickSettingsItems.push(this._vpnToggle);

        this.quickSettingsItems.forEach(toggle => {
            toggle.connectObject(
                'activation-failed', () => this._onActivationFailed(),
                this);
        });

        this._primaryIndicator = this._addIndicator();
        this._vpnIndicator = this._addIndicator();

        this._primaryIndicatorBinding = new GObject.BindingGroup();
        this._primaryIndicatorBinding.bind('icon-name',
            this._primaryIndicator, 'icon-name',
            GObject.BindingFlags.DEFAULT);

        this._vpnToggle.bind_property('checked',
            this._vpnIndicator, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        this._vpnToggle.bind_property('icon-name',
            this._vpnIndicator, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE);

        this._getClient().catch(logError);
    }

    async _getClient() {
        this._client = await NM.Client.new_async(null);

        this.quickSettingsItems.forEach(
            toggle => toggle.setClient(this._client));

        this._client.bind_property('nm-running',
            this, 'visible',
            GObject.BindingFlags.SYNC_CREATE);

        const {connectivityCheckUri} = this._client;
        this._portalHandler = new CaptivePortalHandler(connectivityCheckUri);
        this._portalHandler.connect('recheck', async (o, path) => {
            try {
                const state = await this._client.check_connectivity_async(null);
                if (state >= NM.ConnectivityState.FULL)
                    this._portalHandler.removeConnection(path);
            } catch (e) { }
        });

        this._client.connectObject(
            'notify::primary-connection', () => this._syncMainConnection(),
            'notify::activating-connection', () => this._syncMainConnection(),
            'notify::connectivity', () => this._syncConnectivity(),
            this);
        this._syncMainConnection();

        try {
            this._configPermission = await Polkit.Permission.new(
                'org.freedesktop.NetworkManager.network-control', null, null);

            this.quickSettingsItems.forEach(toggle => {
                this._configPermission.bind_property('allowed',
                    toggle, 'reactive',
                    GObject.BindingFlags.SYNC_CREATE);
            });
        } catch (e) {
            log(`No permission to control network connections: ${e}`);
            this._configPermission = null;
        }
    }

    _onActivationFailed() {
        this._notification?.destroy();

        const source = MessageTray.getSystemSource();
        this._notification = new MessageTray.Notification({
            source,
            title: _('Connection failed'),
            body: _('Activation of network connection failed'),
            iconName: 'network-error-symbolic',
            urgency: MessageTray.Urgency.HIGH,
            isTransient: true,
        });
        this._notification.connect('destroy',
            () => (this._notification = null));

        source.addNotification(this._notification);
    }

    _syncMainConnection() {
        this._mainConnection?.disconnectObject(this);

        this._mainConnection =
            this._client.get_primary_connection() ||
            this._client.get_activating_connection();

        if (this._mainConnection) {
            this._mainConnection.connectObject('notify::state',
                this._mainConnectionStateChanged.bind(this), this);
            this._mainConnectionStateChanged();
        }

        this._updateIcon();
        this._syncConnectivity();
    }

    _mainConnectionStateChanged() {
        if (this._mainConnection.state === NM.ActiveConnectionState.ACTIVATED)
            this._notification?.destroy();
    }

    _syncConnectivity() {
        if (this._mainConnection == null ||
            this._mainConnection.state !== NM.ActiveConnectionState.ACTIVATED) {
            this._portalHandler.clear();
            return;
        }

        let isPortal = this._client.connectivity === NM.ConnectivityState.PORTAL;
        // For testing, allow interpreting any value != FULL as PORTAL, because
        // LIMITED (no upstream route after the default gateway) is easy to obtain
        // with a tethered phone
        // NONE is also possible, with a connection configured to force no default route
        // (but in general we should only prompt a portal if we know there is a portal)
        if (GLib.getenv('GNOME_SHELL_CONNECTIVITY_TEST') != null)
            isPortal ||= this._client.connectivity < NM.ConnectivityState.FULL;
        if (!isPortal || Main.sessionMode.isGreeter)
            return;

        this._portalHandler.addConnection(
            this._mainConnection.get_id(),
            this._mainConnection.get_path());
    }

    _updateIcon() {
        const [dev] = this._mainConnection?.get_devices() ?? [];
        const primaryToggle = this._deviceToggles.get(dev?.device_type) ?? null;
        this._primaryIndicatorBinding.source = primaryToggle;

        if (!primaryToggle) {
            if (this._client.connectivity === NM.ConnectivityState.FULL)
                this._primaryIndicator.icon_name = 'network-wired-symbolic';
            else
                this._primaryIndicator.icon_name = 'network-wired-no-route-symbolic';
        }

        const state = this._client.get_state();
        const connected = state === NM.State.CONNECTED_GLOBAL;
        this._primaryIndicator.visible = (primaryToggle != null) || connected;
    }
});
(uuay)shellMountOperation.jste// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Animation from './animation.js';
import * as CheckBox from './checkBox.js';
import * as Dialog from './dialog.js';
import * as MessageTray from './messageTray.js';
import * as ModalDialog from './modalDialog.js';
import * as Params from '../misc/params.js';
import * as ShellEntry from './shellEntry.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';
import {wiggle} from '../misc/animationUtils.js';

const LIST_ITEM_ICON_SIZE = 48;
const WORK_SPINNER_ICON_SIZE = 16;

const REMEMBER_MOUNT_PASSWORD_KEY = 'remember-mount-password';

/* ------ Common Utils ------- */
function _setButtonsForChoices(dialog, oldChoices, choices) {
    let buttons = [];
    let buttonsChanged = oldChoices.length !== choices.length;

    for (let idx = 0; idx < choices.length; idx++) {
        let button = idx;

        buttonsChanged ||= oldChoices[idx] !== choices[idx];

        buttons.unshift({
            label: choices[idx],
            action: () => dialog.emit('response', button),
        });
    }

    if (buttonsChanged)
        dialog.setButtons(buttons);
}

function _setLabelsForMessage(content, message) {
    let labels = message.split('\n');

    content.title = labels.shift();
    content.description = labels.join('\n');
}

/* -------------------------------------------------------- */

export class ShellMountOperation {
    constructor(source, params) {
        params = Params.parse(params, {existingDialog: null});

        this._dialog = null;
        this._existingDialog = params.existingDialog;
        this._processesDialog = null;

        this.mountOp = new Shell.MountOperation();

        this.mountOp.connect('ask-question',
            this._onAskQuestion.bind(this));
        this.mountOp.connect('ask-password',
            this._onAskPassword.bind(this));
        this.mountOp.connect('show-processes-2',
            this._onShowProcesses2.bind(this));
        this.mountOp.connect('aborted',
            this.close.bind(this));
        this.mountOp.connect('show-unmount-progress',
            this._onShowUnmountProgress.bind(this));

        this._drive = source.get_drive();
        this._drive?.connectObject('disconnected',
            this.close.bind(this), this);
    }

    _closeExistingDialog() {
        if (!this._existingDialog)
            return;

        this._existingDialog.close();
        this._existingDialog = null;
    }

    _onAskQuestion(op, message, choices) {
        this._closeExistingDialog();
        this._dialog = new ShellMountQuestionDialog();

        this._dialog.connectObject('response',
            (object, choice) => {
                this.mountOp.set_choice(choice);
                this.mountOp.reply(Gio.MountOperationResult.HANDLED);

                this.close();
            }, this);

        this._dialog.update(message, choices);
        this._dialog.open();
    }

    _onAskPassword(op, message, defaultUser, defaultDomain, flags) {
        if (this._existingDialog) {
            this._dialog = this._existingDialog;
            this._dialog.reaskPassword();
        } else {
            this._dialog = new ShellMountPasswordDialog(message, flags);
        }

        this._dialog.connectObject('response',
            (object, choice, password, remember, hiddenVolume, systemVolume, pim) => {
                if (choice === -1) {
                    this.mountOp.reply(Gio.MountOperationResult.ABORTED);
                } else {
                    if (remember)
                        this.mountOp.set_password_save(Gio.PasswordSave.PERMANENTLY);
                    else
                        this.mountOp.set_password_save(Gio.PasswordSave.NEVER);

                    this.mountOp.set_password(password);
                    this.mountOp.set_is_tcrypt_hidden_volume(hiddenVolume);
                    this.mountOp.set_is_tcrypt_system_volume(systemVolume);
                    this.mountOp.set_pim(pim);
                    this.mountOp.reply(Gio.MountOperationResult.HANDLED);
                }
            }, this);
        this._dialog.open();
    }

    close(_op) {
        this._closeExistingDialog();
        this._processesDialog = null;

        if (this._dialog) {
            this._dialog.close();
            this._dialog = null;
        }

        if (this._notifier) {
            this._notifier.done();
            this._notifier = null;
        }

        if (this._drive) {
            this._drive.disconnectObject(this);
            this._drive = null;
        }
    }

    _onShowProcesses2(op) {
        this._closeExistingDialog();

        let processes = op.get_show_processes_pids();
        let choices = op.get_show_processes_choices();
        let message = op.get_show_processes_message();

        if (!this._processesDialog) {
            this._processesDialog = new ShellProcessesDialog();
            this._dialog = this._processesDialog;

            this._processesDialog.connectObject('response',
                (object, choice) => {
                    if (choice === -1) {
                        this.mountOp.reply(Gio.MountOperationResult.ABORTED);
                    } else {
                        this.mountOp.set_choice(choice);
                        this.mountOp.reply(Gio.MountOperationResult.HANDLED);
                    }

                    this.close();
                }, this);
            this._processesDialog.open();
        }

        this._processesDialog.update(message, processes, choices);
    }

    _onShowUnmountProgress(op, message, timeLeft, bytesLeft) {
        if (bytesLeft === 0)
            this._showUnmountNotificationDone(message);
        else
            this._showUnmountNotification(message);
    }

    borrowDialog() {
        this._dialog?.disconnectObject(this);
        return this._dialog;
    }

    _createNotification(title, body) {
        this._notification?.destroy();

        const source = MessageTray.getSystemSource();
        this._notification = new MessageTray.Notification({
            source,
            title,
            body,
            isTransient: true,
            iconName: 'media-removable-symbolic',
        });

        this._notification.connect('destroy', () => delete this._notification);
        source.addNotification(this._notification);
    }

    _showUnmountNotificationDone(message) {
        if (message)
            this._createNotification(message, null);
    }

    _showUnmountNotification(message) {
        const [title, body] = message.split('\n', 2);

        if (!this._notification)
            this._createNotification(title, body);
        else
            this._notification.set({title, body});
    }
}

const ShellMountQuestionDialog = GObject.registerClass({
    Signals: {'response': {param_types: [GObject.TYPE_INT]}},
}, class ShellMountQuestionDialog extends ModalDialog.ModalDialog {
    _init() {
        super._init({styleClass: 'mount-question-dialog'});

        this._oldChoices = [];

        this._content = new Dialog.MessageDialogContent();
        this.contentLayout.add_child(this._content);
    }

    vfunc_key_release_event(event) {
        if (event.get_key_symbol() === Clutter.KEY_Escape) {
            this.emit('response', -1);
            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    update(message, choices) {
        _setLabelsForMessage(this._content, message);
        _setButtonsForChoices(this, this._oldChoices, choices);
        this._oldChoices = choices;
    }
});

const ShellMountPasswordDialog = GObject.registerClass({
    Signals: {
        'response': {
            param_types: [
                GObject.TYPE_INT,
                GObject.TYPE_STRING,
                GObject.TYPE_BOOLEAN,
                GObject.TYPE_BOOLEAN,
                GObject.TYPE_BOOLEAN,
                GObject.TYPE_UINT,
            ],
        },
    },
}, class ShellMountPasswordDialog extends ModalDialog.ModalDialog {
    _init(message, flags) {
        let strings = message.split('\n');
        let title = strings.shift() || null;
        let description = strings.shift() || null;
        super._init({styleClass: 'prompt-dialog'});

        let disksApp = Shell.AppSystem.get_default().lookup_app('org.gnome.DiskUtility.desktop');

        let content = new Dialog.MessageDialogContent({title, description});

        let passwordGridLayout = new Clutter.GridLayout({orientation: Clutter.Orientation.VERTICAL});
        let passwordGrid = new St.Widget({
            style_class: 'prompt-dialog-password-grid',
            layout_manager: passwordGridLayout,
        });
        passwordGridLayout.hookup_style(passwordGrid);

        let rtl = passwordGrid.get_text_direction() === Clutter.TextDirection.RTL;
        let curGridRow = 0;

        if (flags & Gio.AskPasswordFlags.TCRYPT) {
            this._hiddenVolume = new CheckBox.CheckBox(_('Hidden Volume'));
            content.add_child(this._hiddenVolume);

            this._systemVolume = new CheckBox.CheckBox(_('Windows System Volume'));
            content.add_child(this._systemVolume);

            this._keyfilesCheckbox = new CheckBox.CheckBox(_('Uses Keyfiles'));
            this._keyfilesCheckbox.connect('clicked', this._onKeyfilesCheckboxClicked.bind(this));
            content.add_child(this._keyfilesCheckbox);

            this._keyfilesLabel = new St.Label({visible: false});
            if (disksApp) {
                this._keyfilesLabel.clutter_text.set_markup(
                    /* Translators: %s is the Disks application */
                    _('To unlock a volume that uses keyfiles, use the <i>%s</i> utility instead.')
                   .format(disksApp.get_name()));
            } else {
                this._keyfilesLabel.clutter_text.set_markup(
                    _('You need an external utility like <i>Disks</i> to unlock a volume that uses keyfiles.'));
            }
            this._keyfilesLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
            this._keyfilesLabel.clutter_text.line_wrap = true;
            content.add_child(this._keyfilesLabel);

            this._pimEntry = new St.PasswordEntry({
                style_class: 'prompt-dialog-password-entry',
                hint_text: _('PIM Number'),
                can_focus: true,
                x_expand: true,
            });
            this._pimEntry.clutter_text.connect('activate', this._onEntryActivate.bind(this));
            ShellEntry.addContextMenu(this._pimEntry);

            if (rtl)
                passwordGridLayout.attach(this._pimEntry, 1, curGridRow, 1, 1);
            else
                passwordGridLayout.attach(this._pimEntry, 0, curGridRow, 1, 1);
            curGridRow += 1;
        } else {
            this._hiddenVolume = null;
            this._systemVolume = null;
            this._pimEntry = null;
        }

        this._passwordEntry = new St.PasswordEntry({
            style_class: 'prompt-dialog-password-entry',
            hint_text: _('Password'),
            can_focus: true,
            x_expand: true,
        });
        this._passwordEntry.clutter_text.connect('activate', this._onEntryActivate.bind(this));
        this.setInitialKeyFocus(this._passwordEntry);
        ShellEntry.addContextMenu(this._passwordEntry);

        this._workSpinner = new Animation.Spinner(WORK_SPINNER_ICON_SIZE, {
            animate: true,
        });

        if (rtl) {
            passwordGridLayout.attach(this._workSpinner, 0, curGridRow, 1, 1);
            passwordGridLayout.attach(this._passwordEntry, 1, curGridRow, 1, 1);
        } else {
            passwordGridLayout.attach(this._passwordEntry, 0, curGridRow, 1, 1);
            passwordGridLayout.attach(this._workSpinner, 1, curGridRow, 1, 1);
        }
        curGridRow += 1;

        let warningBox = new St.BoxLayout({vertical: true});

        let capsLockWarning = new ShellEntry.CapsLockWarning();
        warningBox.add_child(capsLockWarning);

        this._errorMessageLabel = new St.Label({
            style_class: 'prompt-dialog-error-label',
            opacity: 0,
        });
        this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this._errorMessageLabel.clutter_text.line_wrap = true;
        warningBox.add_child(this._errorMessageLabel);

        passwordGridLayout.attach(warningBox, 0, curGridRow, 2, 1);

        content.add_child(passwordGrid);

        if (flags & Gio.AskPasswordFlags.SAVING_SUPPORTED) {
            this._rememberChoice = new CheckBox.CheckBox(_('Remember Password'));
            this._rememberChoice.checked =
                global.settings.get_boolean(REMEMBER_MOUNT_PASSWORD_KEY);
            content.add_child(this._rememberChoice);
        } else {
            this._rememberChoice = null;
        }

        this.contentLayout.add_child(content);

        this._defaultButtons = [{
            label: _('Cancel'),
            action: this._onCancelButton.bind(this),
            key: Clutter.KEY_Escape,
        }, {
            label: _('Unlock'),
            action: this._onUnlockButton.bind(this),
            default: true,
        }];

        this._usesKeyfilesButtons = [{
            label: _('Cancel'),
            action: this._onCancelButton.bind(this),
            key: Clutter.KEY_Escape,
        }];

        if (disksApp) {
            this._usesKeyfilesButtons.push({
                /* Translators: %s is the Disks application */
                label: _('Open %s').format(disksApp.get_name()),
                action: () => {
                    disksApp.activate();
                    this._onCancelButton();
                },
                default: true,
            });
        }

        this.setButtons(this._defaultButtons);
    }

    reaskPassword() {
        this._workSpinner.stop();
        this._passwordEntry.set_text('');
        this._errorMessageLabel.text = _('Sorry, that didn’t work. Please try again.');
        this._errorMessageLabel.opacity = 255;

        wiggle(this._passwordEntry);
    }

    _onCancelButton() {
        this.emit('response', -1, '', false, false, false, 0);
    }

    _onUnlockButton() {
        this._onEntryActivate();
    }

    _onEntryActivate() {
        let pim = 0;
        if (this._pimEntry !== null) {
            pim = this._pimEntry.get_text();

            if (isNaN(pim)) {
                this._pimEntry.set_text('');
                this._errorMessageLabel.text = _('The PIM must be a number or empty.');
                this._errorMessageLabel.opacity = 255;
                return;
            }

            this._errorMessageLabel.opacity = 0;
        }

        global.settings.set_boolean(REMEMBER_MOUNT_PASSWORD_KEY,
            this._rememberChoice && this._rememberChoice.checked);

        this._workSpinner.play();
        this.emit('response', 1,
            this._passwordEntry.get_text(),
            this._rememberChoice &&
            this._rememberChoice.checked,
            this._hiddenVolume &&
            this._hiddenVolume.checked,
            this._systemVolume &&
            this._systemVolume.checked,
            parseInt(pim));
    }

    _onKeyfilesCheckboxClicked() {
        let useKeyfiles = this._keyfilesCheckbox.checked;
        this._passwordEntry.reactive = !useKeyfiles;
        this._passwordEntry.can_focus = !useKeyfiles;
        this._pimEntry.reactive = !useKeyfiles;
        this._pimEntry.can_focus = !useKeyfiles;
        this._rememberChoice.reactive = !useKeyfiles;
        this._rememberChoice.can_focus = !useKeyfiles;
        this._keyfilesLabel.visible = useKeyfiles;
        this.setButtons(useKeyfiles ? this._usesKeyfilesButtons : this._defaultButtons);
    }
});

const ShellProcessesDialog = GObject.registerClass({
    Signals: {'response': {param_types: [GObject.TYPE_INT]}},
}, class ShellProcessesDialog extends ModalDialog.ModalDialog {
    _init() {
        super._init({styleClass: 'processes-dialog'});

        this._oldChoices = [];

        this._content = new Dialog.MessageDialogContent();
        this.contentLayout.add_child(this._content);

        this._applicationSection = new Dialog.ListSection();
        this._applicationSection.hide();
        this.contentLayout.add_child(this._applicationSection);
    }

    vfunc_key_release_event(event) {
        if (event.get_key_symbol() === Clutter.KEY_Escape) {
            this.emit('response', -1);
            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _setAppsForPids(pids) {
        // remove all the items
        this._applicationSection.list.destroy_all_children();

        pids.forEach(pid => {
            let tracker = Shell.WindowTracker.get_default();
            let app = tracker.get_app_from_pid(pid);

            if (!app)
                return;

            let listItem = new Dialog.ListSectionItem({
                icon_actor: app.create_icon_texture(LIST_ITEM_ICON_SIZE),
                title: app.get_name(),
            });
            this._applicationSection.list.add_child(listItem);
        });

        this._applicationSection.visible =
            this._applicationSection.list.get_n_children() > 0;
    }

    update(message, processes, choices) {
        this._setAppsForPids(processes);
        _setLabelsForMessage(this._content, message);
        _setButtonsForChoices(this, this._oldChoices, choices);
        this._oldChoices = choices;
    }
});

const GnomeShellMountOpIface = loadInterfaceXML('org.Gtk.MountOperationHandler');

/** @enum {number} */
const ShellMountOperationType = {
    NONE: 0,
    ASK_PASSWORD: 1,
    ASK_QUESTION: 2,
    SHOW_PROCESSES: 3,
};

export class GnomeShellMountOpHandler {
    constructor() {
        this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellMountOpIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gtk/MountOperationHandler');
        Gio.bus_own_name_on_connection(Gio.DBus.session,
            'org.gtk.MountOperationHandler',
            Gio.BusNameOwnerFlags.REPLACE, null, null);

        this._dialog = null;

        this._ensureEmptyRequest();
    }

    _ensureEmptyRequest() {
        this._currentId = null;
        this._currentInvocation = null;
        this._currentType = ShellMountOperationType.NONE;
    }

    _clearCurrentRequest(response, details) {
        if (this._currentInvocation) {
            this._currentInvocation.return_value(
                GLib.Variant.new('(ua{sv})', [response, details]));
        }

        this._ensureEmptyRequest();
    }

    _setCurrentRequest(invocation, id, type) {
        let oldId = this._currentId;
        let oldType = this._currentType;
        let requestId = `${id}@${invocation.get_sender()}`;

        this._clearCurrentRequest(Gio.MountOperationResult.UNHANDLED, {});

        this._currentInvocation = invocation;
        this._currentId = requestId;
        this._currentType = type;

        if (this._dialog && (oldId === requestId) && (oldType === type))
            return true;

        return false;
    }

    _closeDialog() {
        if (this._dialog) {
            this._dialog.close();
            this._dialog = null;
        }
    }

    /**
     * The dialog will stay visible until clients call the Close() method, or
     * another dialog becomes visible.
     * Calling AskPassword again for the same id will have the effect to clear
     * the existing dialog and update it with a message indicating the previous
     * attempt went wrong.
     *
     * @param {Array} params
     *   {string} id: an opaque ID identifying the object for which
     *       the operation is requested
     *   {string} message: the message to display
     *   {string} icon_name: the name of an icon to display
     *   {string} default_user: the default username for display
     *   {string} default_domain: the default domain for display
     *   {Gio.AskPasswordFlags} flags: a set of GAskPasswordFlags
     *   {Gio.MountOperationResults} response: a GMountOperationResult
     *   {Object} response_details: a dictionary containing response details as
     *       entered by the user. The dictionary MAY contain the following
     *       properties:
     *   - "password" -> (s): a password to be used to complete the mount operation
     *   - "password_save" -> (u): a GPasswordSave
     * @param {Gio.DBusMethodInvocation} invocation
     *      The ID must be unique in the context of the calling process.
     */
    AskPasswordAsync(params, invocation) {
        let [id, message, iconName_, defaultUser_, defaultDomain_, flags] = params;

        if (this._setCurrentRequest(invocation, id, ShellMountOperationType.ASK_PASSWORD)) {
            this._dialog.reaskPassword();
            return;
        }

        this._closeDialog();

        this._dialog = new ShellMountPasswordDialog(message, flags);
        this._dialog.connect('response',
            (object, choice, password, remember, hiddenVolume, systemVolume, pim) => {
                let details = {};
                let response;

                if (choice === -1) {
                    response = Gio.MountOperationResult.ABORTED;
                } else {
                    response = Gio.MountOperationResult.HANDLED;

                    let passSave = remember ? Gio.PasswordSave.PERMANENTLY : Gio.PasswordSave.NEVER;
                    details['password_save'] = GLib.Variant.new('u', passSave);
                    details['password'] = GLib.Variant.new('s', password);
                    details['hidden_volume'] = GLib.Variant.new('b', hiddenVolume);
                    details['system_volume'] = GLib.Variant.new('b', systemVolume);
                    details['pim'] = GLib.Variant.new('u', pim);
                }

                this._clearCurrentRequest(response, details);
            });
        this._dialog.open();
    }

    /**
     * The dialog will stay visible until clients call the Close() method, or
     * another dialog becomes visible.
     * Calling AskQuestion again for the same id will have the effect to clear
     * update the dialog with the new question.
     *
     * @param {Array} params - params
     *   {string} id: an opaque ID identifying the object for which
     *       the operation is requested
     *      The ID must be unique in the context of the calling process.
     *   {string} message: the message to display
     *   {string} icon_name: the name of an icon to display
     *   {string[]} choices: an array of choice strings
     * @param {Gio.DBusMethodInvocation} invocation - invocation
     */
    AskQuestionAsync(params, invocation) {
        let [id, message, iconName_, choices] = params;

        if (this._setCurrentRequest(invocation, id, ShellMountOperationType.ASK_QUESTION)) {
            this._dialog.update(message, choices);
            return;
        }

        this._closeDialog();

        this._dialog = new ShellMountQuestionDialog(message);
        this._dialog.connect('response', (object, choice) => {
            let response;
            let details = {};

            if (choice === -1) {
                response = Gio.MountOperationResult.ABORTED;
            } else {
                response = Gio.MountOperationResult.HANDLED;
                details['choice'] = GLib.Variant.new('i', choice);
            }

            this._clearCurrentRequest(response, details);
        });

        this._dialog.update(message, choices);
        this._dialog.open();
    }

    /**
     * The dialog will stay visible until clients call the Close() method, or
     * another dialog becomes visible.
     * Calling ShowProcesses again for the same id will have the effect to clear
     * the existing dialog and update it with the new message and the new list
     * of processes.
     *
     * @param {Array} params - params
     *   {string} id: an opaque ID identifying the object for which
     *       the operation is requested
     *      The ID must be unique in the context of the calling process.
     *   {string} message: the message to display
     *   {string} icon_name: the name of an icon to display
     *   {number[]} application_pids: the PIDs of the applications to display
     *   {string[]} choices: an array of choice strings
     * @param {Gio.DBusMethodInvocation} invocation - invocation
     */
    ShowProcessesAsync(params, invocation) {
        let [id, message, iconName_, applicationPids, choices] = params;

        if (this._setCurrentRequest(invocation, id, ShellMountOperationType.SHOW_PROCESSES)) {
            this._dialog.update(message, applicationPids, choices);
            return;
        }

        this._closeDialog();

        this._dialog = new ShellProcessesDialog();
        this._dialog.connect('response', (object, choice) => {
            let response;
            let details = {};

            if (choice === -1) {
                response = Gio.MountOperationResult.ABORTED;
            } else {
                response = Gio.MountOperationResult.HANDLED;
                details['choice'] = GLib.Variant.new('i', choice);
            }

            this._clearCurrentRequest(response, details);
        });

        this._dialog.update(message, applicationPids, choices);
        this._dialog.open();
    }

    /**
     * Closes a dialog previously opened by AskPassword, AskQuestion or ShowProcesses.
     * If no dialog is open, does nothing.
     *
     * @param {Array} _params - params
     * @param {Gio.DBusMethodInvocation} _invocation - invocation
     */
    Close(_params, _invocation) {
        this._clearCurrentRequest(Gio.MountOperationResult.UNHANDLED, {});
        this._closeDialog();
    }
}
(uuay)workspaceThumbnail.js��// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import Graphene from 'gi://Graphene';

import * as DND from './dnd.js';
import * as Main from './main.js';
import {TransientSignalHolder} from '../misc/signalTracker.js';
import * as Util from '../misc/util.js';
import * as Workspace from './workspace.js';

const NUM_WORKSPACES_THRESHOLD = 2;

// The maximum size of a thumbnail is 5% the width and height of the screen
export const MAX_THUMBNAIL_SCALE = 0.05;

const RESCALE_ANIMATION_TIME = 200;
const SLIDE_ANIMATION_TIME = 200;

// When we create workspaces by dragging, we add a "cut" into the top and
// bottom of each workspace so that the user doesn't have to hit the
// placeholder exactly.
const WORKSPACE_CUT_SIZE = 10;

const WORKSPACE_KEEP_ALIVE_TIME = 100;

const MUTTER_SCHEMA = 'org.gnome.mutter';

/* A layout manager that requests size only for primary_actor, but then allocates
   all using a fixed layout */
const PrimaryActorLayout = GObject.registerClass(
class PrimaryActorLayout extends Clutter.FixedLayout {
    _init(primaryActor) {
        super._init();

        this.primaryActor = primaryActor;
    }

    vfunc_get_preferred_width(container, forHeight) {
        return this.primaryActor.get_preferred_width(forHeight);
    }

    vfunc_get_preferred_height(container, forWidth) {
        return this.primaryActor.get_preferred_height(forWidth);
    }
});

export const WindowClone = GObject.registerClass({
    Signals: {
        'drag-begin': {},
        'drag-cancelled': {},
        'drag-end': {},
        'selected': {param_types: [GObject.TYPE_UINT]},
    },
}, class WindowClone extends Clutter.Actor {
    _init(realWindow) {
        let clone = new Clutter.Clone({source: realWindow});
        super._init({
            layout_manager: new PrimaryActorLayout(clone),
            reactive: true,
        });
        this._delegate = this;

        this.add_child(clone);
        this.realWindow = realWindow;
        this.metaWindow = realWindow.meta_window;

        this.realWindow.connectObject(
            'notify::position', this._onPositionChanged.bind(this),
            'destroy', () => {
                // First destroy the clone and then destroy everything
                // This will ensure that we never see it in the _disconnectSignals loop
                clone.destroy();
                this.destroy();
            }, this);
        this._onPositionChanged();

        this.connect('destroy', this._onDestroy.bind(this));

        this._draggable = DND.makeDraggable(this, {
            restoreOnSuccess: true,
            dragActorMaxSize: Workspace.WINDOW_DND_SIZE,
            dragActorOpacity: Workspace.DRAGGING_WINDOW_OPACITY,
        });
        this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
        this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
        this._draggable.connect('drag-end', this._onDragEnd.bind(this));
        this.inDrag = false;

        const clickAction = new Clutter.ClickAction();
        clickAction.connect('clicked',
            () => this.emit('selected', Clutter.get_current_event_time()));
        this._draggable.addClickAction(clickAction);

        let iter = win => {
            let actor = win.get_compositor_private();

            if (!actor)
                return false;
            if (!win.is_attached_dialog())
                return false;

            this._doAddAttachedDialog(win, actor);
            win.foreach_transient(iter);

            return true;
        };
        this.metaWindow.foreach_transient(iter);
    }

    // Find the actor just below us, respecting reparenting done
    // by DND code
    getActualStackAbove() {
        if (this._stackAbove == null)
            return null;

        if (this.inDrag) {
            if (this._stackAbove._delegate)
                return this._stackAbove._delegate.getActualStackAbove();
            else
                return null;
        } else {
            return this._stackAbove;
        }
    }

    setStackAbove(actor) {
        this._stackAbove = actor;

        // Don't apply the new stacking now, it will be applied
        // when dragging ends and window are stacked again
        if (actor.inDrag)
            return;

        let parent = this.get_parent();
        let actualAbove = this.getActualStackAbove();
        if (actualAbove == null)
            parent.set_child_below_sibling(this, null);
        else
            parent.set_child_above_sibling(this, actualAbove);
    }

    addAttachedDialog(win) {
        this._doAddAttachedDialog(win, win.get_compositor_private());
    }

    _doAddAttachedDialog(metaDialog, realDialog) {
        let clone = new Clutter.Clone({source: realDialog});
        this._updateDialogPosition(realDialog, clone);

        realDialog.connectObject(
            'notify::position', dialog => this._updateDialogPosition(dialog, clone),
            'destroy', () => clone.destroy(), this);
        this.add_child(clone);
    }

    _updateDialogPosition(realDialog, cloneDialog) {
        let metaDialog = realDialog.meta_window;
        let dialogRect = metaDialog.get_frame_rect();
        let rect = this.metaWindow.get_frame_rect();

        cloneDialog.set_position(dialogRect.x - rect.x, dialogRect.y - rect.y);
    }

    _onPositionChanged() {
        this.set_position(this.realWindow.x, this.realWindow.y);
    }

    _onDestroy() {
        this._delegate = null;

        if (this.inDrag) {
            this.emit('drag-end');
            this.inDrag = false;
        }
    }

    _onDragBegin(_draggable, _time) {
        this.inDrag = true;
        this.emit('drag-begin');
    }

    _onDragCancelled(_draggable, _time) {
        this.emit('drag-cancelled');
    }

    _onDragEnd(_draggable, _time, _snapback) {
        this.inDrag = false;

        // We may not have a parent if DnD completed successfully, in
        // which case our clone will shortly be destroyed and replaced
        // with a new one on the target workspace.
        let parent = this.get_parent();
        if (parent !== null) {
            if (this._stackAbove == null)
                parent.set_child_below_sibling(this, null);
            else
                parent.set_child_above_sibling(this, this._stackAbove);
        }


        this.emit('drag-end');
    }
});


export const ThumbnailState = {
    NEW:            0,
    EXPANDING:      1,
    EXPANDED:       2,
    ANIMATING_IN:   3,
    NORMAL:         4,
    REMOVING:       5,
    ANIMATING_OUT:  6,
    ANIMATED_OUT:   7,
    COLLAPSING:     8,
    DESTROYED:      9,
};

export const WorkspaceThumbnail = GObject.registerClass({
    Properties: {
        'collapse-fraction': GObject.ParamSpec.double(
            'collapse-fraction', 'collapse-fraction', 'collapse-fraction',
            GObject.ParamFlags.READWRITE,
            0, 1, 0),
        'slide-position': GObject.ParamSpec.double(
            'slide-position', 'slide-position', 'slide-position',
            GObject.ParamFlags.READWRITE,
            0, 1, 0),
    },
}, class WorkspaceThumbnail extends St.Widget {
    /**
     * @param {Meta.Workspace} metaWorkspace
     * @param {number} monitorIndex
     */
    _init(metaWorkspace, monitorIndex) {
        super._init({
            clip_to_allocation: true,
            style_class: 'workspace-thumbnail',
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
        });
        this._delegate = this;

        this.metaWorkspace = metaWorkspace;
        this.monitorIndex = monitorIndex;

        this._removed = false;

        this._viewport = new Clutter.Actor();
        this.add_child(this._viewport);

        this._contents = new Clutter.Actor();
        this._viewport.add_child(this._contents);

        this.connect('destroy', this._onDestroy.bind(this));

        let workArea = Main.layoutManager.getWorkAreaForMonitor(this.monitorIndex);
        this.setPorthole(workArea.x, workArea.y, workArea.width, workArea.height);

        let windows = global.get_window_actors().filter(actor => {
            let win = actor.meta_window;
            return win.located_on_workspace(metaWorkspace);
        });

        // Create clones for windows that should be visible in the Overview
        this._windows = [];
        this._allWindows = [];
        for (let i = 0; i < windows.length; i++) {
            windows[i].meta_window.connectObject('notify::minimized',
                this._updateMinimized.bind(this), this);
            this._allWindows.push(windows[i].meta_window);

            if (this._isMyWindow(windows[i]) && this._isOverviewWindow(windows[i]))
                this._addWindowClone(windows[i]);
        }

        // Track window changes
        this.metaWorkspace.connectObject(
            'window-added', this._windowAdded.bind(this),
            'window-removed', this._windowRemoved.bind(this), this);
        global.display.connectObject(
            'window-entered-monitor', this._windowEnteredMonitor.bind(this),
            'window-left-monitor', this._windowLeftMonitor.bind(this), this);

        this.state = ThumbnailState.NORMAL;
        this._slidePosition = 0; // Fully slid in
        this._collapseFraction = 0; // Not collapsed
    }

    setPorthole(x, y, width, height) {
        this._viewport.set_size(width, height);
        this._contents.set_position(-x, -y);
    }

    _lookupIndex(metaWindow) {
        return this._windows.findIndex(w => w.metaWindow === metaWindow);
    }

    syncStacking(stackIndices) {
        this._windows.sort((a, b) => {
            let indexA = stackIndices[a.metaWindow.get_stable_sequence()];
            let indexB = stackIndices[b.metaWindow.get_stable_sequence()];
            return indexA - indexB;
        });

        for (let i = 1; i < this._windows.length; i++) {
            let clone = this._windows[i];
            const previousClone = this._windows[i - 1];
            clone.setStackAbove(previousClone);
        }
    }

    set slidePosition(slidePosition) {
        if (this._slidePosition === slidePosition)
            return;

        const scale = Util.lerp(1, 0.75, slidePosition);
        this.set_scale(scale, scale);
        this.opacity = Util.lerp(255, 0, slidePosition);

        this._slidePosition = slidePosition;
        this.notify('slide-position');
        this.queue_relayout();
    }

    get slidePosition() {
        return this._slidePosition;
    }

    set collapseFraction(collapseFraction) {
        if (this._collapseFraction === collapseFraction)
            return;
        this._collapseFraction = collapseFraction;
        this.notify('collapse-fraction');
        this.queue_relayout();
    }

    get collapseFraction() {
        return this._collapseFraction;
    }

    _doRemoveWindow(metaWin) {
        let clone = this._removeWindowClone(metaWin);
        if (clone)
            clone.destroy();
    }

    _doAddWindow(metaWin) {
        if (this._removed)
            return;

        let win = metaWin.get_compositor_private();

        if (!win) {
            // Newly-created windows are added to a workspace before
            // the compositor finds out about them...
            let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                if (!this._removed &&
                    metaWin.get_compositor_private() &&
                    metaWin.get_workspace() === this.metaWorkspace)
                    this._doAddWindow(metaWin);
                return GLib.SOURCE_REMOVE;
            });
            GLib.Source.set_name_by_id(id, '[gnome-shell] this._doAddWindow');
            return;
        }

        if (!this._allWindows.includes(metaWin)) {
            metaWin.connectObject('notify::minimized',
                this._updateMinimized.bind(this), this);
            this._allWindows.push(metaWin);
        }

        // We might have the window in our list already if it was on all workspaces and
        // now was moved to this workspace
        if (this._lookupIndex(metaWin) !== -1)
            return;

        if (!this._isMyWindow(win))
            return;

        if (this._isOverviewWindow(win)) {
            this._addWindowClone(win);
        } else if (metaWin.is_attached_dialog()) {
            let parent = metaWin.get_transient_for();
            while (parent.is_attached_dialog())
                parent = parent.get_transient_for();

            let idx = this._lookupIndex(parent);
            if (idx < 0) {
                // parent was not created yet, it will take care
                // of the dialog when created
                return;
            }

            let clone = this._windows[idx];
            clone.addAttachedDialog(metaWin);
        }
    }

    _windowAdded(metaWorkspace, metaWin) {
        this._doAddWindow(metaWin);
    }

    _windowRemoved(metaWorkspace, metaWin) {
        let index = this._allWindows.indexOf(metaWin);
        if (index !== -1) {
            metaWin.disconnectObject(this);
            this._allWindows.splice(index, 1);
        }

        this._doRemoveWindow(metaWin);
    }

    _windowEnteredMonitor(metaDisplay, monitorIndex, metaWin) {
        if (monitorIndex === this.monitorIndex)
            this._doAddWindow(metaWin);
    }

    _windowLeftMonitor(metaDisplay, monitorIndex, metaWin) {
        if (monitorIndex === this.monitorIndex)
            this._doRemoveWindow(metaWin);
    }

    _updateMinimized(metaWin) {
        if (metaWin.minimized)
            this._doRemoveWindow(metaWin);
        else
            this._doAddWindow(metaWin);
    }

    workspaceRemoved() {
        if (this._removed)
            return;

        this._removed = true;

        this.metaWorkspace.disconnectObject(this);
        global.display.disconnectObject(this);
        this._allWindows.forEach(w => w.disconnectObject(this));
    }

    _onDestroy() {
        this.workspaceRemoved();
        this._windows = [];
    }

    // Tests if @actor belongs to this workspace and monitor
    _isMyWindow(actor) {
        let win = actor.meta_window;
        return win.located_on_workspace(this.metaWorkspace) &&
            (win.get_monitor() === this.monitorIndex);
    }

    // Tests if @win should be shown in the Overview
    _isOverviewWindow(win) {
        return !win.get_meta_window().skip_taskbar &&
               win.get_meta_window().showing_on_its_workspace();
    }

    // Create a clone of a (non-desktop) window and add it to the window list
    _addWindowClone(win) {
        let clone = new WindowClone(win);

        clone.connect('selected', (o, time) => {
            this.activate(time);
        });
        clone.connect('drag-begin', () => {
            Main.overview.beginWindowDrag(clone.metaWindow);
        });
        clone.connect('drag-cancelled', () => {
            Main.overview.cancelledWindowDrag(clone.metaWindow);
        });
        clone.connect('drag-end', () => {
            Main.overview.endWindowDrag(clone.metaWindow);
        });
        clone.connect('destroy', () => {
            this._removeWindowClone(clone.metaWindow);
        });
        this._contents.add_child(clone);

        if (this._windows.length > 0)
            clone.setStackAbove(this._windows[this._windows.length - 1]);

        this._windows.push(clone);

        return clone;
    }

    _removeWindowClone(metaWin) {
        // find the position of the window in our list
        let index = this._lookupIndex(metaWin);

        if (index === -1)
            return null;

        return this._windows.splice(index, 1).pop();
    }

    activate(time) {
        if (this.state > ThumbnailState.NORMAL)
            return;

        // a click on the already current workspace should go back to the main view
        if (this.metaWorkspace.active)
            Main.overview.hide();
        else
            this.metaWorkspace.activate(time);
    }

    // Draggable target interface used only by ThumbnailsBox
    handleDragOverInternal(source, actor, time) {
        if (source === Main.xdndHandler) {
            this.metaWorkspace.activate(time);
            return DND.DragMotionResult.CONTINUE;
        }

        if (this.state > ThumbnailState.NORMAL)
            return DND.DragMotionResult.CONTINUE;

        if (source.metaWindow &&
            !this._isMyWindow(source.metaWindow.get_compositor_private()))
            return DND.DragMotionResult.MOVE_DROP;
        if (source.app && source.app.can_open_new_window())
            return DND.DragMotionResult.COPY_DROP;
        if (!source.app && source.shellWorkspaceLaunch)
            return DND.DragMotionResult.COPY_DROP;

        return DND.DragMotionResult.CONTINUE;
    }

    acceptDropInternal(source, actor, time) {
        if (this.state > ThumbnailState.NORMAL)
            return false;

        if (source.metaWindow) {
            let win = source.metaWindow.get_compositor_private();
            if (this._isMyWindow(win))
                return false;

            let metaWindow = win.get_meta_window();
            Main.moveWindowToMonitorAndWorkspace(metaWindow,
                this.monitorIndex, this.metaWorkspace.index());
            return true;
        } else if (source.app && source.app.can_open_new_window()) {
            if (source.animateLaunchAtPos)
                source.animateLaunchAtPos(actor.x, actor.y);

            source.app.open_new_window(this.metaWorkspace.index());
            return true;
        } else if (!source.app && source.shellWorkspaceLaunch) {
            // While unused in our own drag sources, shellWorkspaceLaunch allows
            // extensions to define custom actions for their drag sources.
            source.shellWorkspaceLaunch({
                workspace: this.metaWorkspace.index(),
                timestamp: time,
            });
            return true;
        }

        return false;
    }

    setScale(scaleX, scaleY) {
        this._viewport.set_scale(scaleX, scaleY);
    }
});


export const ThumbnailsBox = GObject.registerClass({
    Properties: {
        'expand-fraction': GObject.ParamSpec.double(
            'expand-fraction', 'expand-fraction', 'expand-fraction',
            GObject.ParamFlags.READWRITE,
            0, 1, 1),
        'scale': GObject.ParamSpec.double(
            'scale', 'scale', 'scale',
            GObject.ParamFlags.READWRITE,
            0, Infinity, 0),
        'should-show': GObject.ParamSpec.boolean(
            'should-show', 'should-show', 'should-show',
            GObject.ParamFlags.READABLE,
            true),
    },
}, class ThumbnailsBox extends St.Widget {
    _init(scrollAdjustment, monitorIndex) {
        super._init({
            style_class: 'workspace-thumbnails',
            reactive: true,
            x_align: Clutter.ActorAlign.CENTER,
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
        });

        this._delegate = this;

        let indicator = new St.Bin({style_class: 'workspace-thumbnail-indicator'});

        // We don't want the indicator to affect drag-and-drop
        Shell.util_set_hidden_from_pick(indicator, true);

        this._indicator = indicator;
        this.add_child(indicator);

        this._monitorIndex = monitorIndex;

        this._dropWorkspace = -1;
        this._dropPlaceholderPos = -1;
        this._dropPlaceholder = new St.Bin({style_class: 'placeholder'});
        this.add_child(this._dropPlaceholder);
        this._spliceIndex = -1;

        this._maxThumbnailScale = MAX_THUMBNAIL_SCALE;
        this._targetScale = 0;
        this._scale = 0;
        this._expandFraction = 1;
        this._updateStateId = 0;
        this._pendingScaleUpdate = false;
        this._animatingIndicator = false;

        this._shouldShow = true;

        this._stateCounts = {};
        for (let key in ThumbnailState)
            this._stateCounts[ThumbnailState[key]] = 0;

        this._thumbnails = [];

        const clickAction = new Clutter.ClickAction();
        clickAction.connect('clicked', () => {
            this._activateThumbnailAtPoint(
                ...clickAction.get_coords(),
                Clutter.get_current_event_time());
        });
        this.add_action(clickAction);

        Main.overview.connectObject(
            'showing', () => this._createThumbnails(),
            'hidden', () => this._destroyThumbnails(),
            'item-drag-begin', () => this._onDragBegin(),
            'item-drag-end', () => this._onDragEnd(),
            'item-drag-cancelled', () => this._onDragCancelled(),
            'window-drag-begin', () => this._onDragBegin(),
            'window-drag-end', () => this._onDragEnd(),
            'window-drag-cancelled', () => this._onDragCancelled(), this);

        this._settings = new Gio.Settings({schema_id: MUTTER_SCHEMA});
        this._settings.connect('changed::dynamic-workspaces',
            () => this._updateShouldShow());
        this._updateShouldShow();

        Main.layoutManager.connectObject('monitors-changed', () => {
            this._destroyThumbnails();
            if (Main.overview.visible)
                this._createThumbnails();
        }, this);

        // The porthole is the part of the screen we're showing in the thumbnails
        global.display.connectObject('workareas-changed',
            () => this._updatePorthole(), this);
        this._updatePorthole();

        this.connect('notify::visible', () => {
            if (!this.visible)
                this._queueUpdateStates();
        });
        this.connect('destroy', () => this._onDestroy());

        this._scrollAdjustment = scrollAdjustment;
        this._scrollAdjustment.connectObject('notify::value',
            () => this._updateIndicator(), this);
    }

    get maxThumbnailScale() {
        return this._maxThumbnailScale;
    }

    setMonitorIndex(monitorIndex) {
        this._monitorIndex = monitorIndex;
    }

    _onDestroy() {
        this._destroyThumbnails();
        this._unqueueUpdateStates();

        if (this._settings)
            this._settings.run_dispose();
        this._settings = null;
    }

    _updateShouldShow() {
        const {nWorkspaces} = global.workspace_manager;
        const shouldShow = this._settings.get_boolean('dynamic-workspaces')
            ? nWorkspaces > NUM_WORKSPACES_THRESHOLD
            : nWorkspaces > 1;

        if (this._shouldShow === shouldShow)
            return;

        this._shouldShow = shouldShow;
        this.notify('should-show');
    }

    _updateIndicator() {
        const {value} = this._scrollAdjustment;
        const {workspaceManager} = global;
        const activeIndex = workspaceManager.get_active_workspace_index();

        this._animatingIndicator = value !== activeIndex;

        if (!this._animatingIndicator)
            this._queueUpdateStates();

        this.queue_relayout();
    }

    _activateThumbnailAtPoint(stageX, stageY, time) {
        const [r_, x] = this.transform_stage_point(stageX, stageY);

        const thumbnail = this._thumbnails.find(t => x >= t.x && x <= t.x + t.width);
        if (thumbnail)
            thumbnail.activate(time);
    }

    _onDragBegin() {
        this._dragCancelled = false;
        this._dragMonitor = {
            dragMotion: this._onDragMotion.bind(this),
        };
        DND.addDragMonitor(this._dragMonitor);
    }

    _onDragEnd() {
        if (this._dragCancelled)
            return;

        this._endDrag();
    }

    _onDragCancelled() {
        this._dragCancelled = true;
        this._endDrag();
    }

    _endDrag() {
        this._clearDragPlaceholder();
        DND.removeDragMonitor(this._dragMonitor);
    }

    _onDragMotion(dragEvent) {
        if (!this.contains(dragEvent.targetActor))
            this._onLeave();
        return DND.DragMotionResult.CONTINUE;
    }

    _onLeave() {
        this._clearDragPlaceholder();
    }

    _clearDragPlaceholder() {
        if (this._dropPlaceholderPos === -1)
            return;

        this._dropPlaceholderPos = -1;
        this.queue_relayout();
    }

    _getPlaceholderTarget(index, spacing, rtl) {
        const workspace = this._thumbnails[index];

        let targetX1;
        let targetX2;

        if (rtl) {
            const baseX = workspace.x + workspace.width;
            targetX1 = baseX - WORKSPACE_CUT_SIZE;
            targetX2 = baseX + spacing + WORKSPACE_CUT_SIZE;
        } else {
            targetX1 = workspace.x - spacing - WORKSPACE_CUT_SIZE;
            targetX2 = workspace.x + WORKSPACE_CUT_SIZE;
        }

        if (index === 0) {
            if (rtl)
                targetX2 -= spacing + WORKSPACE_CUT_SIZE;
            else
                targetX1 += spacing + WORKSPACE_CUT_SIZE;
        }

        if (index === this._dropPlaceholderPos) {
            const placeholderWidth = this._dropPlaceholder.get_width() + spacing;
            if (rtl)
                targetX2 += placeholderWidth;
            else
                targetX1 -= placeholderWidth;
        }

        return [targetX1, targetX2];
    }

    _withinWorkspace(x, index, rtl) {
        const length = this._thumbnails.length;
        const workspace = this._thumbnails[index];

        let workspaceX1 = workspace.x + WORKSPACE_CUT_SIZE;
        let workspaceX2 = workspace.x + workspace.width - WORKSPACE_CUT_SIZE;

        if (index === length - 1) {
            if (rtl)
                workspaceX1 -= WORKSPACE_CUT_SIZE;
            else
                workspaceX2 += WORKSPACE_CUT_SIZE;
        }

        return x > workspaceX1 && x <= workspaceX2;
    }

    // Draggable target interface
    handleDragOver(source, actor, x, y, time) {
        if (!source.metaWindow &&
            (!source.app || !source.app.can_open_new_window()) &&
            (source.app || !source.shellWorkspaceLaunch) &&
            source !== Main.xdndHandler)
            return DND.DragMotionResult.CONTINUE;

        const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
        let canCreateWorkspaces = Meta.prefs_get_dynamic_workspaces();
        let spacing = this.get_theme_node().get_length('spacing');

        this._dropWorkspace = -1;
        let placeholderPos = -1;
        let length = this._thumbnails.length;
        for (let i = 0; i < length; i++) {
            const index = rtl ? length - i - 1 : i;

            if (canCreateWorkspaces && source !== Main.xdndHandler) {
                const [targetStart, targetEnd] =
                    this._getPlaceholderTarget(index, spacing, rtl);

                if (x > targetStart && x <= targetEnd) {
                    placeholderPos = index;
                    break;
                }
            }

            if (this._withinWorkspace(x, index, rtl)) {
                this._dropWorkspace = index;
                break;
            }
        }

        if (this._dropPlaceholderPos !== placeholderPos) {
            this._dropPlaceholderPos = placeholderPos;
            this.queue_relayout();
        }

        if (this._dropWorkspace !== -1)
            return this._thumbnails[this._dropWorkspace].handleDragOverInternal(source, actor, time);
        else if (this._dropPlaceholderPos !== -1)
            return source.metaWindow ? DND.DragMotionResult.MOVE_DROP : DND.DragMotionResult.COPY_DROP;
        else
            return DND.DragMotionResult.CONTINUE;
    }

    acceptDrop(source, actor, x, y, time) {
        if (this._dropWorkspace !== -1) {
            return this._thumbnails[this._dropWorkspace].acceptDropInternal(source, actor, time);
        } else if (this._dropPlaceholderPos !== -1) {
            if (!source.metaWindow &&
                (!source.app || !source.app.can_open_new_window()) &&
                (source.app || !source.shellWorkspaceLaunch))
                return false;

            let isWindow = !!source.metaWindow;

            let newWorkspaceIndex;
            [newWorkspaceIndex, this._dropPlaceholderPos] = [this._dropPlaceholderPos, -1];
            this._spliceIndex = newWorkspaceIndex;

            Main.wm.insertWorkspace(newWorkspaceIndex);

            if (isWindow) {
                // Move the window to our monitor first if necessary.
                let thumbMonitor = this._thumbnails[newWorkspaceIndex].monitorIndex;
                Main.moveWindowToMonitorAndWorkspace(source.metaWindow,
                    thumbMonitor, newWorkspaceIndex, true);
            } else if (source.app && source.app.can_open_new_window()) {
                if (source.animateLaunchAtPos)
                    source.animateLaunchAtPos(actor.x, actor.y);

                source.app.open_new_window(newWorkspaceIndex);
            } else if (!source.app && source.shellWorkspaceLaunch) {
                // While unused in our own drag sources, shellWorkspaceLaunch allows
                // extensions to define custom actions for their drag sources.
                source.shellWorkspaceLaunch({
                    workspace: newWorkspaceIndex,
                    timestamp: time,
                });
            }

            if (source.app || (!source.app && source.shellWorkspaceLaunch)) {
                // This new workspace will be automatically removed if the application fails
                // to open its first window within some time, as tracked by Shell.WindowTracker.
                // Here, we only add a very brief timeout to avoid the _immediate_ removal of the
                // workspace while we wait for the startup sequence to load.
                let workspaceManager = global.workspace_manager;
                Main.wm.keepWorkspaceAlive(workspaceManager.get_workspace_by_index(newWorkspaceIndex),
                    WORKSPACE_KEEP_ALIVE_TIME);
            }

            // Start the animation on the workspace (which is actually
            // an old one which just became empty)
            let thumbnail = this._thumbnails[newWorkspaceIndex];
            this._setThumbnailState(thumbnail, ThumbnailState.NEW);
            thumbnail.slide_position = 1;
            thumbnail.collapse_fraction = 1;

            this._queueUpdateStates();

            return true;
        } else {
            return false;
        }
    }

    _createThumbnails() {
        if (this._thumbnails.length > 0)
            return;

        const {workspaceManager} = global;
        this._transientSignalHolder = new TransientSignalHolder(this);
        workspaceManager.connectObject(
            'notify::n-workspaces', this._workspacesChanged.bind(this),
            'active-workspace-changed', () => this._updateIndicator(),
            'workspaces-reordered', () => {
                this._thumbnails.sort((a, b) => {
                    return a.metaWorkspace.index() - b.metaWorkspace.index();
                });
                this.queue_relayout();
            }, this._transientSignalHolder);
        Main.overview.connectObject('windows-restacked',
            this._syncStacking.bind(this), this._transientSignalHolder);

        this._targetScale = 0;
        this._scale = 0;
        this._pendingScaleUpdate = false;
        this._unqueueUpdateStates();

        this._stateCounts = {};
        for (let key in ThumbnailState)
            this._stateCounts[ThumbnailState[key]] = 0;

        this.addThumbnails(0, workspaceManager.n_workspaces);

        this._updateShouldShow();
    }

    _destroyThumbnails() {
        if (this._thumbnails.length === 0)
            return;

        this._transientSignalHolder.destroy();
        delete this._transientSignalHolder;

        for (let w = 0; w < this._thumbnails.length; w++)
            this._thumbnails[w].destroy();
        this._thumbnails = [];
    }

    _workspacesChanged() {
        let validThumbnails =
            this._thumbnails.filter(t => t.state <= ThumbnailState.NORMAL);
        let workspaceManager = global.workspace_manager;
        let oldNumWorkspaces = validThumbnails.length;
        let newNumWorkspaces = workspaceManager.n_workspaces;

        if (newNumWorkspaces > oldNumWorkspaces) {
            this.addThumbnails(oldNumWorkspaces, newNumWorkspaces - oldNumWorkspaces);
        } else {
            let removedIndex;
            let removedNum = oldNumWorkspaces - newNumWorkspaces;
            for (let w = 0; w < oldNumWorkspaces; w++) {
                let metaWorkspace = workspaceManager.get_workspace_by_index(w);
                if (this._thumbnails[w].metaWorkspace !== metaWorkspace) {
                    removedIndex = w;
                    break;
                }
            }

            this.removeThumbnails(removedIndex, removedNum);
        }

        this._updateShouldShow();
    }

    addThumbnails(start, count) {
        let workspaceManager = global.workspace_manager;

        for (let k = start; k < start + count; k++) {
            let metaWorkspace = workspaceManager.get_workspace_by_index(k);
            let thumbnail = new WorkspaceThumbnail(metaWorkspace, this._monitorIndex);
            thumbnail.setPorthole(
                this._porthole.x, this._porthole.y,
                this._porthole.width, this._porthole.height);
            this._thumbnails.push(thumbnail);
            this.add_child(thumbnail);

            if (this._shouldShow && start > 0 && this._spliceIndex === -1) {
                // not the initial fill, and not splicing via DND
                thumbnail.state = ThumbnailState.NEW;
                thumbnail.slide_position = 1; // start slid out
                thumbnail.collapse_fraction = 1; // start fully collapsed
                this._haveNewThumbnails = true;
            } else {
                thumbnail.state = ThumbnailState.NORMAL;
            }

            this._stateCounts[thumbnail.state]++;
        }

        this._queueUpdateStates();

        // The thumbnails indicator actually needs to be on top of the thumbnails
        this.set_child_above_sibling(this._indicator, null);

        // Clear the splice index, we got the message
        this._spliceIndex = -1;
    }

    removeThumbnails(start, count) {
        let currentPos = 0;
        for (let k = 0; k < this._thumbnails.length; k++) {
            let thumbnail = this._thumbnails[k];

            if (thumbnail.state > ThumbnailState.NORMAL)
                continue;

            if (currentPos >= start && currentPos < start + count) {
                thumbnail.workspaceRemoved();
                this._setThumbnailState(thumbnail, ThumbnailState.REMOVING);
            }

            currentPos++;
        }

        this._queueUpdateStates();
    }

    _syncStacking(overview, stackIndices) {
        for (let i = 0; i < this._thumbnails.length; i++)
            this._thumbnails[i].syncStacking(stackIndices);
    }

    set scale(scale) {
        if (this._scale === scale)
            return;

        this._scale = scale;
        this.notify('scale');
        this.queue_relayout();
    }

    get scale() {
        return this._scale;
    }

    _setThumbnailState(thumbnail, state) {
        this._stateCounts[thumbnail.state]--;
        thumbnail.state = state;
        this._stateCounts[thumbnail.state]++;
    }

    _iterateStateThumbnails(state, callback) {
        if (this._stateCounts[state] === 0)
            return;

        for (let i = 0; i < this._thumbnails.length; i++) {
            if (this._thumbnails[i].state === state)
                callback.call(this, this._thumbnails[i]);
        }
    }

    _updateStates() {
        this._updateStateId = 0;

        // If we are animating the indicator, wait
        if (this._animatingIndicator)
            return;

        // Likewise if we are in the process of hiding
        if (!this._shouldShow && this.visible)
            return;

        // Then slide out any thumbnails that have been destroyed
        this._iterateStateThumbnails(ThumbnailState.REMOVING, thumbnail => {
            this._setThumbnailState(thumbnail, ThumbnailState.ANIMATING_OUT);

            thumbnail.ease_property('slide-position', 1, {
                duration: SLIDE_ANIMATION_TIME,
                mode: Clutter.AnimationMode.LINEAR,
                onComplete: () => {
                    this._setThumbnailState(thumbnail, ThumbnailState.ANIMATED_OUT);
                    this._queueUpdateStates();
                },
            });
        });

        // As long as things are sliding out, don't proceed
        if (this._stateCounts[ThumbnailState.ANIMATING_OUT] > 0)
            return;

        // Once that's complete, we can start scaling to the new size,
        // collapse any removed thumbnails and expand added ones
        this._iterateStateThumbnails(ThumbnailState.ANIMATED_OUT, thumbnail => {
            this._setThumbnailState(thumbnail, ThumbnailState.COLLAPSING);
            thumbnail.ease_property('collapse-fraction', 1, {
                duration: RESCALE_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this._stateCounts[thumbnail.state]--;
                    thumbnail.state = ThumbnailState.DESTROYED;

                    let index = this._thumbnails.indexOf(thumbnail);
                    this._thumbnails.splice(index, 1);
                    thumbnail.destroy();

                    this._queueUpdateStates();
                },
            });
        });

        this._iterateStateThumbnails(ThumbnailState.NEW, thumbnail => {
            this._setThumbnailState(thumbnail, ThumbnailState.EXPANDING);
            thumbnail.ease_property('collapse-fraction', 0, {
                duration: SLIDE_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this._setThumbnailState(thumbnail, ThumbnailState.EXPANDED);
                    this._queueUpdateStates();
                },
            });
        });

        if (this._pendingScaleUpdate) {
            this.ease_property('scale', this._targetScale, {
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                duration: RESCALE_ANIMATION_TIME,
                onComplete: () => this._queueUpdateStates(),
            });
            this._pendingScaleUpdate = false;
        }

        // Wait until that's done
        if (this._scale !== this._targetScale ||
            this._stateCounts[ThumbnailState.COLLAPSING] > 0 ||
            this._stateCounts[ThumbnailState.EXPANDING] > 0)
            return;

        // And then slide in any new thumbnails
        this._iterateStateThumbnails(ThumbnailState.EXPANDED, thumbnail => {
            this._setThumbnailState(thumbnail, ThumbnailState.ANIMATING_IN);
            thumbnail.ease_property('slide-position', 0, {
                duration: SLIDE_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this._setThumbnailState(thumbnail, ThumbnailState.NORMAL);
                },
            });
        });
    }

    _queueUpdateStates() {
        if (this._updateStateId > 0)
            return;

        const laters = global.compositor.get_laters();
        this._updateStateId = laters.add(
            Meta.LaterType.BEFORE_REDRAW, () => this._updateStates());
    }

    _unqueueUpdateStates() {
        if (this._updateStateId) {
            const laters = global.compositor.get_laters();
            laters.remove(this._updateStateId);
        }
        this._updateStateId = 0;
    }

    vfunc_get_preferred_height(forWidth) {
        let themeNode = this.get_theme_node();

        forWidth = themeNode.adjust_for_width(forWidth);

        let spacing = themeNode.get_length('spacing');
        let nWorkspaces = this._thumbnails.length;
        let totalSpacing = (nWorkspaces - 1) * spacing;

        const avail = forWidth - totalSpacing;

        let scale = (avail / nWorkspaces) / this._porthole.width;
        scale = Math.min(scale, this._maxThumbnailScale);

        const height = Math.round(this._porthole.height * scale);
        return themeNode.adjust_preferred_height(height, height);
    }

    vfunc_get_preferred_width(_forHeight) {
        // Note that for getPreferredHeight/Width we cheat a bit and skip propagating
        // the size request to our children because we know how big they are and know
        // that the actors aren't depending on the virtual functions being called.
        let themeNode = this.get_theme_node();

        let spacing = themeNode.get_length('spacing');
        let nWorkspaces = this._thumbnails.length;
        let totalSpacing = (nWorkspaces - 1) * spacing;

        const naturalWidth = this._thumbnails.reduce((accumulator, thumbnail, index) => {
            let workspaceSpacing = 0;

            if (index > 0)
                workspaceSpacing += spacing / 2;
            if (index < this._thumbnails.length - 1)
                workspaceSpacing += spacing / 2;

            const progress = 1 - thumbnail.collapse_fraction;
            const width = (this._porthole.width * this._maxThumbnailScale + workspaceSpacing) * progress;
            return accumulator + width;
        }, 0);

        return themeNode.adjust_preferred_width(totalSpacing, naturalWidth);
    }

    _updatePorthole() {
        if (!Main.layoutManager.monitors[this._monitorIndex]) {
            const {x, y, width, height} = global.stage;
            this._porthole = {x, y, width, height};
        } else {
            this._porthole =
                Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex);
        }

        this.queue_relayout();
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        let rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;

        if (this._thumbnails.length === 0) // not visible
            return;

        let themeNode = this.get_theme_node();
        box = themeNode.get_content_box(box);

        const portholeWidth = this._porthole.width;
        const portholeHeight = this._porthole.height;
        const spacing = themeNode.get_length('spacing');

        const nWorkspaces = this._thumbnails.length;

        // Compute the scale we'll need once everything is updated,
        // unless we are currently transitioning
        if (this._expandFraction === 1) {
            const totalSpacing = (nWorkspaces - 1) * spacing;
            const availableWidth = (box.get_width() - totalSpacing) / nWorkspaces;

            const hScale = availableWidth / portholeWidth;
            const vScale = box.get_height() / portholeHeight;
            const newScale = Math.min(hScale, vScale);

            if (newScale !== this._targetScale) {
                if (this._targetScale > 0) {
                    // We don't ease immediately because we need to observe the
                    // ordering in queueUpdateStates - if workspaces have been
                    // removed we need to slide them out as the first thing.
                    this._targetScale = newScale;
                    this._pendingScaleUpdate = true;
                } else {
                    this._targetScale = this._scale = newScale;
                }

                this._queueUpdateStates();
            }
        }

        const ratio = portholeWidth / portholeHeight;
        const thumbnailFullHeight = Math.round(portholeHeight * this._scale);
        const thumbnailWidth = Math.round(thumbnailFullHeight * ratio);
        const thumbnailHeight = thumbnailFullHeight * this._expandFraction;
        const roundedVScale = thumbnailHeight / portholeHeight;

        // We always request size for maxThumbnailScale, distribute
        // space evently if we use smaller thumbnails
        const extraWidth =
            (this._maxThumbnailScale * portholeWidth - thumbnailWidth) * nWorkspaces;
        box.x1 += Math.round(extraWidth / 2);
        box.x2 -= Math.round(extraWidth / 2);

        let indicatorValue = this._scrollAdjustment.value;
        let indicatorUpperWs = Math.ceil(indicatorValue);
        let indicatorLowerWs = Math.floor(indicatorValue);

        let indicatorLowerX1 = 0;
        let indicatorLowerX2 = 0;
        let indicatorUpperX1 = 0;
        let indicatorUpperX2 = 0;

        let indicatorThemeNode = this._indicator.get_theme_node();
        let indicatorTopFullBorder = indicatorThemeNode.get_padding(St.Side.TOP) + indicatorThemeNode.get_border_width(St.Side.TOP);
        let indicatorBottomFullBorder = indicatorThemeNode.get_padding(St.Side.BOTTOM) + indicatorThemeNode.get_border_width(St.Side.BOTTOM);
        let indicatorLeftFullBorder = indicatorThemeNode.get_padding(St.Side.LEFT) + indicatorThemeNode.get_border_width(St.Side.LEFT);
        let indicatorRightFullBorder = indicatorThemeNode.get_padding(St.Side.RIGHT) + indicatorThemeNode.get_border_width(St.Side.RIGHT);

        let x = box.x1;

        if (this._dropPlaceholderPos === -1) {
            this._dropPlaceholder.allocate_preferred_size(
                ...this._dropPlaceholder.get_position());

            const laters = global.compositor.get_laters();
            laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                this._dropPlaceholder.hide();
            });
        }

        let childBox = new Clutter.ActorBox();

        for (let i = 0; i < this._thumbnails.length; i++) {
            const thumbnail = this._thumbnails[i];
            if (i > 0)
                x += spacing - Math.round(thumbnail.collapse_fraction * spacing);

            const y1 = box.y1;
            const y2 = y1 + thumbnailHeight;

            if (i === this._dropPlaceholderPos) {
                const [, placeholderWidth] = this._dropPlaceholder.get_preferred_width(-1);
                childBox.y1 = y1;
                childBox.y2 = y2;

                if (rtl) {
                    childBox.x2 = box.x2 - Math.round(x);
                    childBox.x1 = box.x2 - Math.round(x + placeholderWidth);
                } else {
                    childBox.x1 = Math.round(x);
                    childBox.x2 = Math.round(x + placeholderWidth);
                }

                this._dropPlaceholder.allocate(childBox);

                const laters = global.compositor.get_laters();
                laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                    this._dropPlaceholder.show();
                });
                x += placeholderWidth + spacing;
            }

            // We might end up with thumbnailWidth being something like 99.33
            // pixels. To make this work and not end up with a gap at the end,
            // we need some thumbnails to be 99 pixels and some 100 pixels width;
            // we compute an actual scale separately for each thumbnail.
            const x1 = Math.round(x);
            const x2 = Math.round(x + thumbnailWidth);
            const roundedHScale = (x2 - x1) / portholeWidth;

            // Allocating a scaled actor is funny - x1/y1 correspond to the origin
            // of the actor, but x2/y2 are increased by the *unscaled* size.
            if (rtl) {
                childBox.x2 = box.x2 - x1;
                childBox.x1 = box.x2 - (x1 + thumbnailWidth);
            } else {
                childBox.x1 = x1;
                childBox.x2 = x1 + thumbnailWidth;
            }
            childBox.y1 = y1;
            childBox.y2 = y1 + thumbnailHeight;

            thumbnail.setScale(roundedHScale, roundedVScale);
            thumbnail.allocate(childBox);

            if (i === indicatorUpperWs) {
                indicatorUpperX1 = childBox.x1;
                indicatorUpperX2 = childBox.x2;
            }
            if (i === indicatorLowerWs) {
                indicatorLowerX1 = childBox.x1;
                indicatorLowerX2 = childBox.x2;
            }

            // We round the collapsing portion so that we don't get thumbnails resizing
            // during an animation due to differences in rounded, but leave the uncollapsed
            // portion unrounded so that non-animating we end up with the right total
            x += thumbnailWidth - Math.round(thumbnailWidth * thumbnail.collapse_fraction);
        }

        childBox.y1 = box.y1;
        childBox.y2 = box.y1 + thumbnailHeight;

        const indicatorX1 = indicatorLowerX1 +
            (indicatorUpperX1 - indicatorLowerX1) * (indicatorValue % 1);
        const indicatorX2 = indicatorLowerX2 +
            (indicatorUpperX2 - indicatorLowerX2) * (indicatorValue % 1);

        childBox.x1 = indicatorX1 - indicatorLeftFullBorder;
        childBox.x2 = indicatorX2 + indicatorRightFullBorder;
        childBox.y1 -= indicatorTopFullBorder;
        childBox.y2 += indicatorBottomFullBorder;
        this._indicator.allocate(childBox);
    }

    get shouldShow() {
        return this._shouldShow;
    }

    set expandFraction(expandFraction) {
        if (this._expandFraction === expandFraction)
            return;
        this._expandFraction = expandFraction;
        this.notify('expand-fraction');
        this.queue_relayout();
    }

    get expandFraction() {
        return this._expandFraction;
    }
});
(uuay)appMenu.jsX%// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as AppFavorites from './appFavorites.js';
import * as Main from './main.js';
import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
import * as PopupMenu from './popupMenu.js';

export class AppMenu extends PopupMenu.PopupMenu {
    /**
     * @param {Clutter.Actor} sourceActor - actor the menu is attached to
     * @param {St.Side} side - arrow side
     * @param {object} params - options
     * @param {bool} params.favoritesSection - show items to add/remove favorite
     * @param {bool} params.showSingleWindow - show window section for a single window
     */
    constructor(sourceActor, side = St.Side.TOP, params = {}) {
        if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL) {
            if (side === St.Side.LEFT)
                side = St.Side.RIGHT;
            else if (side === St.Side.RIGHT)
                side = St.Side.LEFT;
        }

        super(sourceActor, 0.5, side);

        this.actor.add_style_class_name('app-menu');

        const {
            favoritesSection = false,
            showSingleWindows = false,
        } = params;

        this._app = null;
        this._appSystem = Shell.AppSystem.get_default();
        this._parentalControlsManager = ParentalControlsManager.getDefault();
        this._appFavorites = AppFavorites.getAppFavorites();
        this._enableFavorites = favoritesSection;
        this._showSingleWindows = showSingleWindows;

        this._windowsChangedId = 0;
        this._updateWindowsLaterId = 0;

        /* Translators: This is the heading of a list of open windows */
        this._openWindowsHeader = new PopupMenu.PopupSeparatorMenuItem(_('Open Windows'));
        this.addMenuItem(this._openWindowsHeader);

        this._windowSection = new PopupMenu.PopupMenuSection();
        this.addMenuItem(this._windowSection);

        this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._newWindowItem = this.addAction(_('New Window'), () => {
            this._animateLaunch();
            this._app.open_new_window(-1);
            Main.overview.hide();
        });

        this._actionSection = new PopupMenu.PopupMenuSection();
        this.addMenuItem(this._actionSection);

        this._onGpuMenuItem = this.addAction('', () => {
            this._animateLaunch();
            this._app.launch(0, -1, this._getNonDefaultLaunchGpu());
            Main.overview.hide();
        });

        this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._toggleFavoriteItem = this.addAction('', () => {
            const appId = this._app.get_id();
            if (this._appFavorites.isFavorite(appId))
                this._appFavorites.removeFavorite(appId);
            else
                this._appFavorites.addFavorite(appId);
        });

        this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._detailsItem = this.addAction(_('App Details'), async () => {
            const id = this._app.get_id();
            const args = GLib.Variant.new('(ss)', [id, '']);
            const bus = await Gio.DBus.get(Gio.BusType.SESSION, null);
            bus.call(
                'org.gnome.Software',
                '/org/gnome/Software',
                'org.gtk.Actions', 'Activate',
                new GLib.Variant('(sava{sv})', ['details', [args], null]),
                null, 0, -1, null);
            Main.overview.hide();
        });

        this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._quitItem =
            this.addAction(_('Quit'), () => this._app.request_quit());

        this._appSystem.connectObject(
            'installed-changed', () => this._updateDetailsVisibility(),
            'app-state-changed', this._onAppStateChanged.bind(this),
            this.actor);

        this._parentalControlsManager.connectObject(
            'app-filter-changed', () => this._updateFavoriteItem(), this.actor);

        this._appFavorites.connectObject(
            'changed', () => this._updateFavoriteItem(), this.actor);

        global.settings.connectObject(
            'writable-changed::favorite-apps', () => this._updateFavoriteItem(),
            this.actor);

        global.connectObject(
            'notify::switcheroo-control', () => this._updateGpuItem(),
            this.actor);

        this._updateQuitItem();
        this._updateFavoriteItem();
        this._updateGpuItem();
        this._updateDetailsVisibility();
    }

    _onAppStateChanged(sys, app) {
        if (this._app !== app)
            return;

        this._updateQuitItem();
        this._updateNewWindowItem();
        this._updateGpuItem();
    }

    _updateQuitItem() {
        this._quitItem.visible = this._app?.state === Shell.AppState.RUNNING;
    }

    _updateNewWindowItem() {
        const actions = this._app?.appInfo?.list_actions() ?? [];
        this._newWindowItem.visible =
            this._app?.can_open_new_window() && !actions.includes('new-window');
    }

    _updateFavoriteItem() {
        const appInfo = this._app?.app_info;
        const canFavorite = appInfo &&
            this._enableFavorites &&
            global.settings.is_writable('favorite-apps') &&
            this._parentalControlsManager.shouldShowApp(appInfo);

        this._toggleFavoriteItem.visible = canFavorite;

        if (!canFavorite)
            return;

        const {id} = this._app;
        this._toggleFavoriteItem.label.text = this._appFavorites.isFavorite(id)
            ? _('Unpin')
            : _('Pin to Dash');
    }

    _updateGpuItem() {
        const proxy = global.get_switcheroo_control();
        const hasDualGpu = proxy?.get_cached_property('HasDualGpu')?.unpack();

        const showItem =
            this._app?.state === Shell.AppState.STOPPED && hasDualGpu;

        this._onGpuMenuItem.visible = showItem;

        if (!showItem)
            return;

        const launchGpu = this._getNonDefaultLaunchGpu();
        this._onGpuMenuItem.label.text = launchGpu === Shell.AppLaunchGpu.DEFAULT
            ? _('Launch using Integrated Graphics Card')
            : _('Launch using Discrete Graphics Card');
    }

    _updateDetailsVisibility() {
        const sw = this._appSystem.lookup_app('org.gnome.Software.desktop');
        this._detailsItem.visible = sw !== null;
    }

    _animateLaunch() {
        if (this.sourceActor.animateLaunch)
            this.sourceActor.animateLaunch();
    }

    _getNonDefaultLaunchGpu() {
        return this._app.appInfo.get_boolean('PrefersNonDefaultGPU')
            ? Shell.AppLaunchGpu.DEFAULT
            : Shell.AppLaunchGpu.DISCRETE;
    }

    /** */
    destroy() {
        this.setApp(null);
        super.destroy();
    }

    /**
     * @returns {bool} - true if the menu is empty
     */
    isEmpty() {
        if (!this._app)
            return true;
        return super.isEmpty();
    }

    /**
     * @param {Shell.App} app - the app the menu represents
     */
    setApp(app) {
        if (this._app === app)
            return;

        this._app?.disconnectObject(this);

        this._app = app;

        this._app?.connectObject('windows-changed',
            () => this._queueUpdateWindowsSection(), this);

        this._updateWindowsSection();

        const appInfo = app?.app_info;
        const actions = appInfo?.list_actions() ?? [];

        this._actionSection.removeAll();
        actions.forEach(action => {
            const label = appInfo.get_action_name(action);
            this._actionSection.addAction(label, event => {
                if (action === 'new-window')
                    this._animateLaunch();

                this._app.launch_action(action, event.get_time(), -1);
                Main.overview.hide();
            });
        });

        this._updateQuitItem();
        this._updateNewWindowItem();
        this._updateFavoriteItem();
        this._updateGpuItem();
    }

    _queueUpdateWindowsSection() {
        if (this._updateWindowsLaterId)
            return;

        const laters = global.compositor.get_laters();
        this._updateWindowsLaterId = laters.add(
            Meta.LaterType.BEFORE_REDRAW, () => {
                this._updateWindowsSection();
                return GLib.SOURCE_REMOVE;
            });
    }

    _updateWindowsSection() {
        if (this._updateWindowsLaterId) {
            const laters = global.compositor.get_laters();
            laters.remove(this._updateWindowsLaterId);
        }
        this._updateWindowsLaterId = 0;

        this._windowSection.removeAll();
        this._openWindowsHeader.hide();

        if (!this._app)
            return;

        const minWindows = this._showSingleWindows ? 1 : 2;
        const windows = this._app.get_windows().filter(w => !w.skip_taskbar);
        if (windows.length < minWindows)
            return;

        this._openWindowsHeader.show();

        windows.forEach(window => {
            const title = window.title || this._app.get_name();
            const item = this._windowSection.addAction(title, event => {
                Main.activateWindow(window, event.get_time());
            });
            window.connectObject('notify::title', () => {
                item.label.text = window.title || this._app.get_name();
            }, item);
        });
    }
}
(uuay)extensionSystem.js�t// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import St from 'gi://St';
import Shell from 'gi://Shell';
import * as Signals from '../misc/signals.js';

import * as Config from '../misc/config.js';
import * as Desktop from '../misc/desktop.js';
import * as ExtensionDownloader from './extensionDownloader.js';
import {formatError} from '../misc/errorUtils.js';
import {ExtensionState, ExtensionType} from '../misc/extensionUtils.js';
import * as FileUtils from '../misc/fileUtils.js';
import * as Main from './main.js';
import * as MessageTray from './messageTray.js';

const ENABLED_EXTENSIONS_KEY = 'enabled-extensions';
const DISABLED_EXTENSIONS_KEY = 'disabled-extensions';
const DISABLE_USER_EXTENSIONS_KEY = 'disable-user-extensions';
const EXTENSION_DISABLE_VERSION_CHECK_KEY = 'disable-extension-version-validation';

const UPDATE_CHECK_TIMEOUT = 24 * 60 * 60; // 1 day in seconds

function stateToString(state) {
    return Object.keys(ExtensionState).find(k => ExtensionState[k] === state);
}

export class ExtensionManager extends Signals.EventEmitter {
    constructor() {
        super();

        this._initializationPromise = null;
        this._updateNotified = false;
        this._updateInProgress = false;
        this._updatedUUIDS = [];

        this._extensions = new Map();
        this._unloadedExtensions = new Map();
        this._enabledExtensions = [];
        this._extensionOrder = [];
        this._checkVersion = false;

        St.Settings.get().connect('notify::color-scheme',
            () => this._reloadExtensionStylesheets());

        Main.sessionMode.connect('updated', () => {
            this._sessionUpdated();
        });
    }

    init() {
        // The following file should exist for a period of time when extensions
        // are enabled after start. If it exists, then the systemd unit will
        // disable extensions should gnome-shell crash.
        // Should the file already exist from a previous login, then this is OK.
        let disableFilename = GLib.build_filenamev([GLib.get_user_runtime_dir(), 'gnome-shell-disable-extensions']);
        let disableFile = Gio.File.new_for_path(disableFilename);
        try {
            disableFile.create(Gio.FileCreateFlags.REPLACE_DESTINATION, null);
        } catch (e) {
            log(`Failed to create file ${disableFilename}: ${e.message}`);
        }

        const shutdownId = global.connect('shutdown',
            () => disableFile.delete(null));

        GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 60, () => {
            global.disconnect(shutdownId);

            disableFile.delete(null);
            return GLib.SOURCE_REMOVE;
        });

        this._installExtensionUpdates();
        this._sessionUpdated().then(() => {
            ExtensionDownloader.checkForUpdates();
        });

        GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, UPDATE_CHECK_TIMEOUT, () => {
            ExtensionDownloader.checkForUpdates();

            return GLib.SOURCE_CONTINUE;
        });
    }

    get updatesSupported() {
        const appSys = Shell.AppSystem.get_default();
        const hasUpdatesApp =
            appSys.lookup_app('org.gnome.Extensions.desktop') !== null ||
            appSys.lookup_app('com.mattjakeman.ExtensionManager.desktop') !== null;
        const allowed = global.settings.get_boolean('allow-extension-installation');
        return allowed && hasUpdatesApp;
    }

    lookup(uuid) {
        return this._extensions.get(uuid);
    }

    getUuids() {
        return [...this._extensions.keys()];
    }

    _reloadExtensionStylesheets() {
        for (const ext of this._extensions.values()) {
            // No stylesheet, nothing to reload
            if (!ext.stylesheet)
                continue;

            // No variants, so skip reloading
            const path = ext.stylesheet.get_path();
            if (!path.endsWith('-dark.css') && !path.endsWith('-light.css'))
                continue;

            try {
                this._unloadExtensionStylesheet(ext);
                this._loadExtensionStylesheet(ext);
            } catch (e) {
                this._callExtensionDisable(ext.uuid);
                this.logExtensionError(ext.uuid, e);
            }
        }
    }

    _loadExtensionStylesheet(extension) {
        if (extension.state !== ExtensionState.ACTIVE &&
            extension.state !== ExtensionState.ACTIVATING)
            return;

        const variant = Main.getStyleVariant();
        const stylesheetNames = [
            `${global.sessionMode}-${variant}.css`,
            `stylesheet-${variant}.css`,
            `${global.sessionMode}.css`,
            'stylesheet.css',
        ];
        const theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
        for (const name of stylesheetNames) {
            try {
                const stylesheetFile = extension.dir.get_child(name);
                theme.load_stylesheet(stylesheetFile);
                extension.stylesheet = stylesheetFile;
                break;
            } catch (e) {
                if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
                    continue; // not an error
                throw e;
            }
        }
    }

    _unloadExtensionStylesheet(extension) {
        if (!extension.stylesheet)
            return;

        const theme = St.ThemeContext.get_for_stage(global.stage).get_theme();
        theme.unload_stylesheet(extension.stylesheet);
        delete extension.stylesheet;
    }

    _changeExtensionState(extension, newState) {
        const strState = stateToString(newState);
        console.debug(`Changing state of extension ${extension.uuid} to ${strState}`);

        extension.state = newState;
        this.emit('extension-state-changed', extension);
    }

    _extensionSupportsSessionMode(uuid) {
        const extension = this.lookup(uuid);

        if (!extension)
            return false;

        if (extension.sessionModes.includes(Main.sessionMode.currentMode))
            return true;

        if (extension.sessionModes.includes(Main.sessionMode.parentMode))
            return true;

        return false;
    }

    async _callExtensionDisable(uuid) {
        let extension = this.lookup(uuid);
        if (!extension)
            return;

        if (extension.state !== ExtensionState.ACTIVE)
            return;

        this._changeExtensionState(extension, ExtensionState.DEACTIVATING);

        // "Rebase" the extension order by disabling and then enabling extensions
        // in order to help prevent conflicts.

        // Example:
        //   order = [A, B, C, D, E]
        //   user disables C
        //   this should: disable E, disable D, disable C, enable D, enable E

        let orderIdx = this._extensionOrder.indexOf(uuid);
        let order = this._extensionOrder.slice(orderIdx + 1);
        let orderReversed = order.slice().reverse();

        for (let i = 0; i < orderReversed.length; i++) {
            let otherUuid = orderReversed[i];
            try {
                console.debug(`Temporarily disable extension ${otherUuid}`);
                this.lookup(otherUuid).stateObj.disable();
            } catch (e) {
                this.logExtensionError(otherUuid, e);
            }
        }

        try {
            extension.stateObj.disable();
        } catch (e) {
            this.logExtensionError(uuid, e);
        }

        this._unloadExtensionStylesheet(extension);

        for (let i = 0; i < order.length; i++) {
            let otherUuid = order[i];
            try {
                console.debug(`Re-enable extension ${otherUuid}`);
                // eslint-disable-next-line no-await-in-loop
                await this.lookup(otherUuid).stateObj.enable();
            } catch (e) {
                this.logExtensionError(otherUuid, e);
            }
        }

        this._extensionOrder.splice(orderIdx, 1);

        if (extension.state !== ExtensionState.ERROR)
            this._changeExtensionState(extension, ExtensionState.INACTIVE);
    }

    async _callExtensionEnable(uuid) {
        if (!this._extensionSupportsSessionMode(uuid))
            return;

        let extension = this.lookup(uuid);
        if (!extension)
            return;

        if (extension.state === ExtensionState.INITIALIZED)
            await this._callExtensionInit(uuid);


        if (extension.state !== ExtensionState.INACTIVE)
            return;

        this._changeExtensionState(extension, ExtensionState.ACTIVATING);

        try {
            this._loadExtensionStylesheet(extension);
        } catch (e) {
            this.logExtensionError(uuid, e);
            return;
        }

        try {
            await extension.stateObj.enable();
            this._changeExtensionState(extension, ExtensionState.ACTIVE);
            this._extensionOrder.push(uuid);
        } catch (e) {
            this._unloadExtensionStylesheet(extension);
            this.logExtensionError(uuid, e);
        }
    }

    enableExtension(uuid) {
        if (!this._extensions.has(uuid))
            return false;

        let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
        let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY);

        if (disabledExtensions.includes(uuid)) {
            disabledExtensions = disabledExtensions.filter(item => item !== uuid);
            global.settings.set_strv(DISABLED_EXTENSIONS_KEY, disabledExtensions);
        }

        if (!enabledExtensions.includes(uuid)) {
            enabledExtensions.push(uuid);
            global.settings.set_strv(ENABLED_EXTENSIONS_KEY, enabledExtensions);
        }

        return true;
    }

    disableExtension(uuid) {
        if (!this._extensions.has(uuid))
            return false;

        let enabledExtensions = global.settings.get_strv(ENABLED_EXTENSIONS_KEY);
        let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY);

        if (enabledExtensions.includes(uuid)) {
            enabledExtensions = enabledExtensions.filter(item => item !== uuid);
            global.settings.set_strv(ENABLED_EXTENSIONS_KEY, enabledExtensions);
        }

        if (!disabledExtensions.includes(uuid)) {
            disabledExtensions.push(uuid);
            global.settings.set_strv(DISABLED_EXTENSIONS_KEY, disabledExtensions);
        }

        return true;
    }

    openExtensionPrefs(uuid, parentWindow, options) {
        const extension = this.lookup(uuid);
        if (!extension || !extension.hasPrefs)
            return false;

        Gio.DBus.session.call(
            'org.gnome.Shell.Extensions',
            '/org/gnome/Shell/Extensions',
            'org.gnome.Shell.Extensions',
            'OpenExtensionPrefs',
            new GLib.Variant('(ssa{sv})', [uuid, parentWindow, options]),
            null,
            Gio.DBusCallFlags.NONE,
            -1,
            null);
        return true;
    }

    notifyExtensionUpdate(uuid) {
        if (this._updateInProgress) {
            this._updatedUUIDS.push(uuid);
            return;
        }

        let extension = this.lookup(uuid);
        if (!extension)
            return;

        extension.hasUpdate = true;
        this.emit('extension-state-changed', extension);

        if (!this._updateNotified) {
            this._updateNotified = true;

            let source = new ExtensionUpdateSource();
            Main.messageTray.add(source);

            const notification = new MessageTray.Notification({
                source,
                title: _('Extension Updates Available'),
                body: _('Extension updates are ready to be installed.'),
            });
            notification.connect('activated',
                () => source.open());
            source.addNotification(notification);
        }
    }

    logExtensionError(uuid, error) {
        let extension = this.lookup(uuid);
        if (!extension)
            return;

        const message = formatError(error, {showStack: false});

        console.debug(`Changing state of extension ${uuid} to ERROR`);
        extension.error = message;
        extension.state = ExtensionState.ERROR;
        if (!extension.errors)
            extension.errors = [];
        extension.errors.push(message);

        logError(error, `Extension ${uuid}`);
        this.emit('extension-state-changed', extension);
    }

    createExtensionObject(uuid, dir, type) {
        let metadataFile = dir.get_child('metadata.json');
        if (!metadataFile.query_exists(null))
            throw new Error('Missing metadata.json');

        let metadataContents, success_;
        try {
            [success_, metadataContents] = metadataFile.load_contents(null);
            metadataContents = new TextDecoder().decode(metadataContents);
        } catch (e) {
            throw new Error(`Failed to load metadata.json: ${e}`);
        }
        let meta;
        try {
            meta = JSON.parse(metadataContents);
        } catch (e) {
            throw new Error(`Failed to parse metadata.json: ${e}`);
        }

        const requiredProperties = [{
            prop: 'uuid',
            typeName: 'string',
        }, {
            prop: 'name',
            typeName: 'string',
        }, {
            prop: 'description',
            typeName: 'string',
        }, {
            prop: 'shell-version',
            typeName: 'string array',
            typeCheck: v => Array.isArray(v) && v.length > 0 && v.every(e => typeof e === 'string'),
        }];
        for (let i = 0; i < requiredProperties.length; i++) {
            const {
                prop, typeName, typeCheck = v => typeof v === typeName,
            } = requiredProperties[i];

            if (!meta[prop])
                throw new Error(`missing "${prop}" property in metadata.json`);
            if (!typeCheck(meta[prop]))
                throw new Error(`property "${prop}" is not of type ${typeName}`);
        }

        if (uuid !== meta.uuid)
            throw new Error(`uuid "${meta.uuid}" from metadata.json does not match directory name "${uuid}"`);

        let extension = {
            metadata: meta,
            uuid: meta.uuid,
            type,
            dir,
            path: dir.get_path(),
            error: '',
            hasPrefs: dir.get_child('prefs.js').query_exists(null),
            enabled: this._enabledExtensions.includes(uuid),
            hasUpdate: false,
            canChange: false,
            sessionModes: meta['session-modes'] ? meta['session-modes'] : ['user'],
        };
        this._extensions.set(uuid, extension);

        return extension;
    }

    _canLoad(extension) {
        if (!this._unloadedExtensions.has(extension.uuid))
            return true;

        const version = this._unloadedExtensions.get(extension.uuid);
        return extension.metadata.version === version;
    }

    _isOutOfDate(extension) {
        const [major] = Config.PACKAGE_VERSION.split('.');
        return !extension.metadata['shell-version'].some(v => v.startsWith(major));
    }

    async loadExtension(extension) {
        const {uuid} = extension;
        console.debug(`Loading extension ${uuid}`);
        // Default to error, we set success as the last step
        extension.state = ExtensionState.ERROR;

        if (this._checkVersion && this._isOutOfDate(extension)) {
            extension.state = ExtensionState.OUT_OF_DATE;
        } else if (!this._canLoad(extension)) {
            this.logExtensionError(uuid, new Error(
                'A different version was loaded previously. You need to log out for changes to take effect.'));
        } else {
            const enabled = this._enabledExtensions.includes(uuid) &&
                            this._extensionSupportsSessionMode(uuid);
            if (enabled) {
                if (!await this._callExtensionInit(uuid))
                    return;

                if (extension.state === ExtensionState.INACTIVE)
                    await this._callExtensionEnable(uuid);
            } else {
                extension.state = ExtensionState.INITIALIZED;
            }

            this._unloadedExtensions.delete(uuid);
        }

        console.debug(`Extension ${uuid} in state ${stateToString(extension.state)} after loading`);
        this._updateCanChange(extension);
        this.emit('extension-state-changed', extension);
    }

    async unloadExtension(extension) {
        const {uuid, type} = extension;

        // Try to disable it -- if it's ERROR'd, we can't guarantee that,
        // but it will be removed on next reboot, and hopefully nothing
        // broke too much.
        await this._callExtensionDisable(uuid);

        this._changeExtensionState(extension, ExtensionState.UNINSTALLED);

        // The extension is now cached and it's impossible to load a different version
        if (type === ExtensionType.PER_USER && extension.isImported)
            this._unloadedExtensions.set(uuid, extension.metadata.version);

        this._extensions.delete(uuid);
        return true;
    }

    async reloadExtension(oldExtension) {
        // Grab the things we'll need to pass to createExtensionObject
        // to reload it.
        let {uuid, dir, type} = oldExtension;

        // Then unload the old extension.
        await this.unloadExtension(oldExtension);

        // Now, recreate the extension and load it.
        let newExtension;
        try {
            newExtension = this.createExtensionObject(uuid, dir, type);
        } catch (e) {
            this.logExtensionError(uuid, e);
            return;
        }

        await this.loadExtension(newExtension);
    }

    isModeExtension(uuid) {
        return this._getModeExtensions().indexOf(uuid) !== -1;
    }

    async _callExtensionInit(uuid) {
        if (!this._extensionSupportsSessionMode(uuid))
            return false;

        let extension = this.lookup(uuid);
        if (!extension)
            throw new Error('Extension was not properly created. Call createExtensionObject first');

        let dir = extension.dir;
        let extensionJs = dir.get_child('extension.js');
        if (!extensionJs.query_exists(null)) {
            this.logExtensionError(uuid, new Error('Missing extension.js'));
            return false;
        }

        let extensionModule;
        let extensionState = null;

        try {
            extensionModule = await import(extensionJs.get_uri());

            // Extensions can only be imported once, so add a property to avoid
            // attempting to re-import an extension.
            extension.isImported = true;
        } catch (e) {
            this.logExtensionError(uuid, e);
            return false;
        }

        try {
            const {metadata, path} = extension;
            extensionState =
                new extensionModule.default({...metadata, dir, path});
        } catch (e) {
            this.logExtensionError(uuid, e);
            return false;
        }

        extension.stateObj = extensionState;
        this._changeExtensionState(extension, ExtensionState.INACTIVE);
        return true;
    }

    _getModeExtensions() {
        if (Array.isArray(Main.sessionMode.enabledExtensions))
            return Main.sessionMode.enabledExtensions;
        return [];
    }

    _updateCanChange(extension) {
        let isMode = this._getModeExtensions().includes(extension.uuid);
        let modeOnly = global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY);

        let changeKey = isMode
            ? DISABLE_USER_EXTENSIONS_KEY
            : ENABLED_EXTENSIONS_KEY;

        extension.canChange =
            global.settings.is_writable(changeKey) &&
            (isMode || !modeOnly);
    }

    _getEnabledExtensions() {
        let extensions = this._getModeExtensions();

        if (!global.settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY))
            extensions = extensions.concat(global.settings.get_strv(ENABLED_EXTENSIONS_KEY));

        extensions.sort((a, b) => this._compareExtensions(this.lookup(a), this.lookup(b)));

        // filter out 'disabled-extensions' which takes precedence
        let disabledExtensions = global.settings.get_strv(DISABLED_EXTENSIONS_KEY);
        return extensions.filter(item => !disabledExtensions.includes(item));
    }

    async _onUserExtensionsEnabledChanged() {
        await this._onEnabledExtensionsChanged();
        this._onSettingsWritableChanged();
    }

    async _onEnabledExtensionsChanged() {
        let newEnabledExtensions = this._getEnabledExtensions();

        for (const extension of this._extensions.values()) {
            const wasEnabled = extension.enabled;
            extension.enabled = newEnabledExtensions.includes(extension.uuid);
            if (wasEnabled !== extension.enabled)
                this.emit('extension-state-changed', extension);
        }

        // Find and enable all the newly enabled extensions: UUIDs found in the
        // new setting, but not in the old one.
        const extensionsToEnable = newEnabledExtensions
            .filter(uuid => !this._enabledExtensions.includes(uuid) &&
                             this._extensionSupportsSessionMode(uuid));
        for (const uuid of extensionsToEnable) {
            // eslint-disable-next-line no-await-in-loop
            await this._callExtensionEnable(uuid);
        }

        // Find and disable all the newly disabled extensions: UUIDs found in the
        // old setting, but not in the new one.
        const extensionsToDisable = this._extensionOrder
            .filter(uuid => !newEnabledExtensions.includes(uuid) ||
                            !this._extensionSupportsSessionMode(uuid));
        // Reverse mutates the original array, but .filter() creates a new array.
        extensionsToDisable.reverse();

        for (const uuid of extensionsToDisable) {
            // eslint-disable-next-line no-await-in-loop
            await this._callExtensionDisable(uuid);
        }

        this._enabledExtensions = newEnabledExtensions;
    }

    _onSettingsWritableChanged() {
        for (let extension of this._extensions.values()) {
            this._updateCanChange(extension);
            this.emit('extension-state-changed', extension);
        }
    }

    async _onVersionValidationChanged() {
        const checkVersion = !global.settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY);
        if (checkVersion === this._checkVersion)
            return;

        this._checkVersion = checkVersion;

        // Disabling extensions modifies the order array, so use a copy
        let extensionOrder = this._extensionOrder.slice();

        // Disable enabled extensions first to avoid
        // the "rebasing" done in _callExtensionDisable...
        this._disableAllExtensions();

        // ...and then reload and enable extensions in the correct order again.
        const extensionsToReload = [...this._extensions.values()].sort((a, b) => {
            return extensionOrder.indexOf(a.uuid) - extensionOrder.indexOf(b.uuid);
        });
        for (const extension of extensionsToReload) {
            // eslint-disable-next-line no-await-in-loop
            await this.reloadExtension(extension);
        }
    }

    async _handleMajorUpdate() {
        const [majorVersion] = Config.PACKAGE_VERSION.split('.');
        const path = `${global.userdatadir}/update-check-${majorVersion}`;
        const file = Gio.File.new_for_path(path);

        try {
            if (!await file.touch_async())
                return;
        } catch (e) {
            logError(e);
        }

        this._updateInProgress = true;

        await ExtensionDownloader.checkForUpdates();
        this._installExtensionUpdates();

        this._updatedUUIDS.map(uuid => this.lookup(uuid)).forEach(
            ext => this.reloadExtension(ext));
        this._updatedUUIDS = [];

        this._updateInProgress = false;
    }

    _installExtensionUpdates() {
        if (!this.updatesSupported)
            return;

        for (const {dir, info} of FileUtils.collectFromDatadirs('extension-updates', true)) {
            let fileType = info.get_file_type();
            if (fileType !== Gio.FileType.DIRECTORY)
                continue;
            let uuid = info.get_name();
            let extensionDir = Gio.File.new_for_path(
                GLib.build_filenamev([global.userdatadir, 'extensions', uuid]));

            try {
                FileUtils.recursivelyDeleteDir(extensionDir, false);
                FileUtils.recursivelyMoveDir(dir, extensionDir);
            } catch (e) {
                log(`Failed to install extension updates for ${uuid}`);
            } finally {
                FileUtils.recursivelyDeleteDir(dir, true);
            }
        }
    }

    _compareExtensions(a, b) {
        const modesA = a?.sessionModes ?? [];
        const modesB = b?.sessionModes ?? [];
        return modesB.length - modesA.length;
    }

    async _loadExtensions() {
        global.settings.connect(`changed::${ENABLED_EXTENSIONS_KEY}`, () => {
            this._onEnabledExtensionsChanged();
        });
        global.settings.connect(`changed::${DISABLED_EXTENSIONS_KEY}`, () => {
            this._onEnabledExtensionsChanged();
        });
        global.settings.connect(`changed::${DISABLE_USER_EXTENSIONS_KEY}`, () => {
            this._onUserExtensionsEnabledChanged();
        });
        global.settings.connect(`changed::${EXTENSION_DISABLE_VERSION_CHECK_KEY}`, () => {
            this._onVersionValidationChanged();
        });
        global.settings.connect(`writable-changed::${ENABLED_EXTENSIONS_KEY}`, () =>
            this._onSettingsWritableChanged());
        global.settings.connect(`writable-changed::${DISABLED_EXTENSIONS_KEY}`, () =>
            this._onSettingsWritableChanged());

        await this._onVersionValidationChanged();

        this._enabledExtensions = this._getEnabledExtensions();

        let perUserDir = Gio.File.new_for_path(global.userdatadir);

        const includeUserDir = global.settings.get_boolean('allow-extension-installation');
        const extensionFiles = [...FileUtils.collectFromDatadirs('extensions', includeUserDir)];
        const extensionObjects = extensionFiles.map(({dir, info}) => {
            let fileType = info.get_file_type();
            if (fileType !== Gio.FileType.DIRECTORY)
                return null;
            let uuid = info.get_name();
            let existing = this.lookup(uuid);
            if (existing) {
                log(`Extension ${uuid} already installed in ${existing.path}. ${dir.get_path()} will not be loaded`);
                return null;
            }

            let extension;
            let type = dir.has_prefix(perUserDir)
                ? ExtensionType.PER_USER
                : ExtensionType.SYSTEM;
            if (Desktop.is('ubuntu') && this.isModeExtension(uuid) && type === ExtensionType.PER_USER) {
                log(`Found user extension ${uuid}, but not loading from ${dir.get_path()} directory as part of session mode.`);
                return;
            }
            try {
                extension = this.createExtensionObject(uuid, dir, type);
            } catch (error) {
                logError(error, `Could not load extension ${uuid}`);
                return null;
            }

            return extension;
        }).filter(extension => extension !== null).sort(this._compareExtensions.bind(this));

        // after updating to a new major version,
        // update extensions before loading them
        await this._handleMajorUpdate();

        for (const extension of extensionObjects) {
            // eslint-disable-next-line no-await-in-loop
            await this.loadExtension(extension);
        }
    }

    async _enableAllExtensions() {
        if (!this._initializationPromise)
            this._initializationPromise = this._loadExtensions();

        await this._initializationPromise;

        for (const uuid of this._enabledExtensions) {
            // eslint-disable-next-line no-await-in-loop
            await this._callExtensionEnable(uuid);
        }
    }


    /**
     * Disables all currently enabled extensions.
     */
    async _disableAllExtensions() {
        // Wait for extensions to finish loading before starting
        // to disable, otherwise some extensions may enable after
        // this function.
        if (this._initializationPromise)
            await this._initializationPromise;

        const extensionsToDisable = this._extensionOrder.slice();
        // Extensions are disabled in the reverse order
        // from when they were enabled.
        extensionsToDisable.reverse();

        for (const uuid of extensionsToDisable) {
            // eslint-disable-next-line no-await-in-loop
            await this._callExtensionDisable(uuid);
        }
    }

    async _sessionUpdated() {
        // Take care of added or removed sessionMode extensions
        await this._onEnabledExtensionsChanged();
        await this._enableAllExtensions();
    }
}

const ExtensionUpdateSource = GObject.registerClass(
class ExtensionUpdateSource extends MessageTray.Source {
    constructor() {
        const appSys = Shell.AppSystem.get_default();
        const app =
            appSys.lookup_app('org.gnome.Extensions.desktop') ||
            appSys.lookup_app('com.mattjakeman.ExtensionManager.desktop');

        super({
            title: app.get_name(),
            icon: app.get_icon(),
            policy: MessageTray.NotificationPolicy.newForApp(app),
        });

        this._app = app;
    }

    open() {
        this._app.activate();
        Main.overview.hide();
        Main.panel.closeCalendar();
    }
});
(uuay)messageTray.js/�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Calendar from './calendar.js';
import * as GnomeSession from '../misc/gnomeSession.js';
import * as Layout from './layout.js';
import * as Main from './main.js';
import * as MessageList from './messageList.js';
import * as SignalTracker from '../misc/signalTracker.js';

const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';

export const ANIMATION_TIME = 200;

const NOTIFICATION_TIMEOUT = 4000;

const HIDE_TIMEOUT = 200;
const LONGER_HIDE_TIMEOUT = 600;

const MAX_NOTIFICATIONS_IN_QUEUE = 3;
const MAX_NOTIFICATIONS_PER_SOURCE = 3;

// We delay hiding of the tray if the mouse is within MOUSE_LEFT_ACTOR_THRESHOLD
// range from the point where it left the tray.
const MOUSE_LEFT_ACTOR_THRESHOLD = 20;

const IDLE_TIME = 1000;

export const State = {
    HIDDEN:  0,
    SHOWING: 1,
    SHOWN:   2,
    HIDING:  3,
};

// These reasons are useful when we destroy the notifications received through
// the notification daemon. We use EXPIRED for notifications that we dismiss
// and the user did not interact with, DISMISSED for all other notifications
// that were destroyed as a result of a user action, SOURCE_CLOSED for the
// notifications that were requested to be destroyed by the associated source,
// and REPLACED for notifications that were destroyed as a consequence of a
// newer version having replaced them.
/** @enum {number} */
export const NotificationDestroyedReason = {
    EXPIRED: 1,
    DISMISSED: 2,
    SOURCE_CLOSED: 3,
    REPLACED: 4,
};

// Message tray has its custom Urgency enumeration. LOW, NORMAL and CRITICAL
// urgency values map to the corresponding values for the notifications received
// through the notification daemon.
/** @enum {number} */
export const Urgency = {
    LOW: 0,
    NORMAL: 1,
    HIGH: 2,
    CRITICAL: 3,
};

// The privacy of the details of a notification. USER is for notifications which
// contain private information to the originating user account (for example,
// details of an e-mail they’ve received). SYSTEM is for notifications which
// contain information private to the physical system (for example, battery
// status) and hence the same for every user. This affects whether the content
// of a notification is shown on the lock screen.
/** @enum {number} */
export const PrivacyScope = {
    USER: 0,
    SYSTEM: 1,
};

class FocusGrabber {
    constructor(actor) {
        this._actor = actor;
        this._prevKeyFocusActor = null;
        this._focused = false;
    }

    grabFocus() {
        if (this._focused)
            return;

        this._prevKeyFocusActor = global.stage.get_key_focus();

        global.stage.connectObject('notify::key-focus',
            this._focusActorChanged.bind(this), this);

        if (!this._actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false))
            this._actor.grab_key_focus();

        this._focused = true;
    }

    _focusUngrabbed() {
        if (!this._focused)
            return false;

        global.stage.disconnectObject(this);

        this._focused = false;
        return true;
    }

    _focusActorChanged() {
        let focusedActor = global.stage.get_key_focus();
        if (!focusedActor || !this._actor.contains(focusedActor))
            this._focusUngrabbed();
    }

    ungrabFocus() {
        if (!this._focusUngrabbed())
            return;

        if (this._prevKeyFocusActor) {
            global.stage.set_key_focus(this._prevKeyFocusActor);
            this._prevKeyFocusActor = null;
        } else {
            let focusedActor = global.stage.get_key_focus();
            if (focusedActor && this._actor.contains(focusedActor))
                global.stage.set_key_focus(null);
        }
    }
}

// NotificationPolicy:
// An object that holds all bits of configurable policy related to a notification
// source, such as whether to play sound or honour the critical bit.
//
// A notification without a policy object will inherit the default one.
export const NotificationPolicy = GObject.registerClass({
    GTypeFlags: GObject.TypeFlags.ABSTRACT,
    Properties: {
        'enable': GObject.ParamSpec.boolean(
            'enable', 'enable', 'enable', GObject.ParamFlags.READABLE, true),
        'enable-sound': GObject.ParamSpec.boolean(
            'enable-sound', 'enable-sound', 'enable-sound',
            GObject.ParamFlags.READABLE, true),
        'show-banners': GObject.ParamSpec.boolean(
            'show-banners', 'show-banners', 'show-banners',
            GObject.ParamFlags.READABLE, true),
        'force-expanded': GObject.ParamSpec.boolean(
            'force-expanded', 'force-expanded', 'force-expanded',
            GObject.ParamFlags.READABLE, false),
        'show-in-lock-screen': GObject.ParamSpec.boolean(
            'show-in-lock-screen', 'show-in-lock-screen', 'show-in-lock-screen',
            GObject.ParamFlags.READABLE, false),
        'details-in-lock-screen': GObject.ParamSpec.boolean(
            'details-in-lock-screen', 'details-in-lock-screen', 'details-in-lock-screen',
            GObject.ParamFlags.READABLE, false),
    },
}, class NotificationPolicy extends GObject.Object {
    /**
     * Create a new policy for app.
     *
     * This will be a NotificationApplicationPolicy for valid apps,
     * or a NotificationGenericPolicy otherwise.
     *
     * @param {Shell.App=} app
     * @returns {NotificationPolicy}
     */
    static newForApp(app) {
        // fallback to generic policy
        if (!app?.get_app_info())
            return new NotificationGenericPolicy();

        const id = app.get_id().replace(/\.desktop$/, '');
        return new NotificationApplicationPolicy(id);
    }

    // Do nothing for the default policy. These methods are only useful for the
    // GSettings policy.
    store() { }

    destroy() {
        this.run_dispose();
    }

    get enable() {
        return true;
    }

    get enableSound() {
        return true;
    }

    get showBanners() {
        return true;
    }

    get forceExpanded() {
        return false;
    }

    get showInLockScreen() {
        return false;
    }

    get detailsInLockScreen() {
        return false;
    }
});

export const NotificationGenericPolicy = GObject.registerClass({
}, class NotificationGenericPolicy extends NotificationPolicy {
    _init() {
        super._init();
        this.id = 'generic';

        this._masterSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.notifications'});
        this._masterSettings.connect('changed', this._changed.bind(this));
    }

    destroy() {
        this._masterSettings.run_dispose();

        super.destroy();
    }

    _changed(settings, key) {
        if (this.constructor.find_property(key))
            this.notify(key);
    }

    get showBanners() {
        return this._masterSettings.get_boolean('show-banners');
    }

    get showInLockScreen() {
        return this._masterSettings.get_boolean('show-in-lock-screen');
    }
});

export const NotificationApplicationPolicy = GObject.registerClass({
}, class NotificationApplicationPolicy extends NotificationPolicy {
    _init(id) {
        super._init();

        this.id = id;
        this._canonicalId = this._canonicalizeId(id);

        this._masterSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.notifications'});
        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.notifications.application',
            path: `/org/gnome/desktop/notifications/application/${this._canonicalId}/`,
        });

        this._masterSettings.connect('changed', this._changed.bind(this));
        this._settings.connect('changed', this._changed.bind(this));
    }

    store() {
        this._settings.set_string('application-id', `${this.id}.desktop`);

        let apps = this._masterSettings.get_strv('application-children');
        if (!apps.includes(this._canonicalId)) {
            apps.push(this._canonicalId);
            this._masterSettings.set_strv('application-children', apps);
        }
    }

    destroy() {
        this._masterSettings.run_dispose();
        this._settings.run_dispose();

        super.destroy();
    }

    _changed(settings, key) {
        if (this.constructor.find_property(key))
            this.notify(key);
    }

    _canonicalizeId(id) {
        // Keys are restricted to lowercase alphanumeric characters and dash,
        // and two dashes cannot be in succession
        return id.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-');
    }

    get enable() {
        return this._settings.get_boolean('enable');
    }

    get enableSound() {
        return this._settings.get_boolean('enable-sound-alerts');
    }

    get showBanners() {
        return this._masterSettings.get_boolean('show-banners') &&
            this._settings.get_boolean('show-banners');
    }

    get forceExpanded() {
        return this._settings.get_boolean('force-expanded');
    }

    get showInLockScreen() {
        return this._masterSettings.get_boolean('show-in-lock-screen') &&
            this._settings.get_boolean('show-in-lock-screen');
    }

    get detailsInLockScreen() {
        return this._settings.get_boolean('details-in-lock-screen');
    }
});

export const Sound = GObject.registerClass(
class Sound extends GObject.Object {
    constructor(file, themedName) {
        super();

        this._soundFile = file;
        this._soundName = themedName;
    }

    play() {
        const player = global.display.get_sound_player();

        if (this._soundName)
            player.play_from_theme(this._soundName, _('Notification sound'), null);
        else if (this._soundFile)
            player.play_from_file(this._soundFile, _('Notification sound'), null);
    }
});

export const Action = GObject.registerClass(
class Action extends GObject.Object {
    constructor(label, callback) {
        super();

        this._label = label;
        this._callback = callback;
    }

    get label() {
        return this._label;
    }

    activate() {
        this._callback();
    }
});

export class Notification extends GObject.Object {
    constructor(params) {
        super(params);

        this._actions = [];

        if (!this.datetime)
            this.datetime = GLib.DateTime.new_now_local();

        // Automatically update the datetime property when the notification
        // is updated.
        this.connect('notify', (o, pspec) => {
            if (pspec.name === 'acknowledged') {
                // Don't update datetime property
            } else if (pspec.name === 'datetime') {
                if (this._updateDatetimeId)
                    GLib.source_remove(this._updateDatetimeId);
                delete this._updateDatetimeId;
            } else if (!this._updateDatetimeId) {
                this._updateDatetimeId =
                    GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                        delete this._updateDatetimeId;
                        this.datetime = GLib.DateTime.new_now_local();
                        return GLib.SOURCE_REMOVE;
                    });
            }
        });
    }

    get actions() {
        return this._actions;
    }

    get iconName() {
        if (this.gicon instanceof Gio.ThemedIcon)
            return this.gicon.iconName;
        else
            return null;
    }

    set iconName(iconName) {
        this.gicon = new Gio.ThemedIcon({name: iconName});
    }

    get privacyScope() {
        return this._privacyScope;
    }

    set privacyScope(privacyScope) {
        if (!Object.values(PrivacyScope).includes(privacyScope))
            throw new Error('out of range');

        if (this._privacyScope === privacyScope)
            return;

        this._privacyScope = privacyScope;
        this.notify('privacy-scope');
    }

    get urgency() {
        return this._urgency;
    }

    set urgency(urgency) {
        if (!Object.values(Urgency).includes(urgency))
            throw new Error('out of range');

        if (this._urgency === urgency)
            return;

        this._urgency = urgency;
        this.notify('urgency');
    }

    // addAction:
    // @label: the label for the action's button
    // @callback: the callback for the action
    addAction(label, callback) {
        const action = new Action(label, () => {
            callback();

            // We don't hide a resident notification when the user invokes one of its actions,
            // because it is common for such notifications to update themselves with new
            // information based on the action. We'd like to display the updated information
            // in place, rather than pop-up a new notification.
            if (this.resident)
                return;

            this.destroy();
        });
        this._actions.push(action);
        this.emit('action-added', action);
    }

    clearActions() {
        if (this._actions.length === 0)
            return;

        this._actions.forEach(action => {
            this.emit('action-removed', action);
        });
        this._actions = [];
    }

    playSound() {
        if (!this.source.policy.enableSound)
            return;

        this.sound?.play(this.title);
    }

    activate() {
        this.emit('activated');

        // We don't hide a resident notification when the user invokes one of its actions,
        // because it is common for such notifications to update themselves with new
        // information based on the action. We'd like to display the updated information
        // in place, rather than pop-up a new notification.
        if (this.resident)
            return;

        this.destroy();
    }

    destroy(reason = NotificationDestroyedReason.DISMISSED) {
        this.emit('destroy', reason);
        this.run_dispose();
    }
}

export const Source = GObject.registerClass({
    Properties: {
        'count': GObject.ParamSpec.int(
            'count', 'count', 'count',
            GObject.ParamFlags.READABLE,
            0, GLib.MAXINT32, 0),
        'policy': GObject.ParamSpec.object(
            'policy', 'policy', 'policy',
            GObject.ParamFlags.READWRITE,
            NotificationPolicy.$gtype),
    },
    Signals: {
        'destroy': {param_types: [GObject.TYPE_UINT]},
        'notification-added': {param_types: [Notification.$gtype]},
        'notification-removed': {param_types: [Notification.$gtype]},
        'notification-request-banner': {param_types: [Notification.$gtype]},
    },
}, class Source extends MessageList.Source {
    constructor(params) {
        super(params);

        this.notifications = [];

        if (!this._policy)
            this._policy = new NotificationGenericPolicy();
    }

    get policy() {
        return this._policy;
    }

    set policy(policy) {
        if (this._policy)
            this._policy.destroy();
        this._policy = policy;
    }

    get count() {
        return this.notifications.length;
    }

    get unseenCount() {
        return this.notifications.filter(n => !n.acknowledged).length;
    }

    get countVisible() {
        return this.count > 1;
    }

    countUpdated() {
        this.notify('count');
    }

    get narrowestPrivacyScope() {
        return this.notifications.every(n => n.privacyScope === PrivacyScope.SYSTEM)
            ? PrivacyScope.SYSTEM
            : PrivacyScope.USER;
    }

    _onNotificationDestroy(notification) {
        let index = this.notifications.indexOf(notification);
        if (index < 0)
            throw new Error('Notification was already removed previously');

        this.notifications.splice(index, 1);
        this.emit('notification-removed', notification);
        this.countUpdated();

        if (!this._inDestruction && this.notifications.length === 0)
            this.destroy();
    }

    addNotification(notification) {
        if (this.notifications.includes(notification))
            return;

        while (this.notifications.length >= MAX_NOTIFICATIONS_PER_SOURCE) {
            const [oldest] = this.notifications;
            oldest.destroy(NotificationDestroyedReason.EXPIRED);
        }

        notification.connect('destroy', this._onNotificationDestroy.bind(this));
        notification.connect('notify::acknowledged', () => {
            this.countUpdated();

            // If acknowledged was set to false try to show the notification again
            if (!notification.acknowledged)
                this.emit('notification-request-banner', notification);
        });
        this.notifications.push(notification);

        this.emit('notification-added', notification);
        this.emit('notification-request-banner', notification);
    }

    destroy(reason) {
        this._inDestruction = true;

        while (this.notifications.length > 0) {
            const [oldest] = this.notifications;
            oldest.destroy(reason);
        }

        this.emit('destroy', reason);

        this.policy.destroy();
        this.run_dispose();
    }

    // To be overridden by subclasses
    open() {
    }

    destroyNonResidentNotifications() {
        for (let i = this.notifications.length - 1; i >= 0; i--) {
            if (!this.notifications[i].resident)
                this.notifications[i].destroy();
        }
    }
});
SignalTracker.registerDestroyableType(Source);

GObject.registerClass({
    Properties: {
        'source': GObject.ParamSpec.object(
            'source', 'source', 'source',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            Source),
        'title': GObject.ParamSpec.string(
            'title', 'title', 'title',
            GObject.ParamFlags.READWRITE,
            null),
        'body': GObject.ParamSpec.string(
            'body', 'body', 'body',
            GObject.ParamFlags.READWRITE,
            null),
        'use-body-markup': GObject.ParamSpec.boolean(
            'use-body-markup', 'use-body-markup', 'use-body-markup',
            GObject.ParamFlags.READWRITE,
            false),
        'gicon': GObject.ParamSpec.object(
            'gicon', 'gicon', 'gicon',
            GObject.ParamFlags.READWRITE,
            Gio.Icon),
        'icon-name': GObject.ParamSpec.string(
            'icon-name', 'icon-name', 'icon-name',
            GObject.ParamFlags.READWRITE,
            null),
        'sound': GObject.ParamSpec.object(
            'sound', 'sound', 'sound',
            GObject.ParamFlags.READWRITE,
            Sound),
        'datetime': GObject.ParamSpec.boxed(
            'datetime', 'datetime', 'datetime',
            GObject.ParamFlags.READWRITE,
            GLib.DateTime),
        // Unfortunately we can't register new enum types in GJS
        // See: https://gitlab.gnome.org/GNOME/gjs/-/issues/573
        'privacy-scope': GObject.ParamSpec.int(
            'privacy-scope', 'privacy-scope', 'privacy-scope',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
            0, GLib.MAXINT32,
            PrivacyScope.User),
        'urgency': GObject.ParamSpec.int(
            'urgency', 'urgency', 'urgency',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
            0, GLib.MAXINT32,
            Urgency.NORMAL),
        'acknowledged': GObject.ParamSpec.boolean(
            'acknowledged', 'acknowledged', 'acknowledged',
            GObject.ParamFlags.READWRITE,
            false),
        'resident': GObject.ParamSpec.boolean(
            'resident', 'resident', 'resident',
            GObject.ParamFlags.READWRITE,
            false),
        'for-feedback': GObject.ParamSpec.boolean(
            'for-feedback', 'for-feedback', 'for-feedback',
            GObject.ParamFlags.READWRITE,
            false),
        'is-transient': GObject.ParamSpec.boolean(
            'is-transient', 'is-transient', 'is-transient',
            GObject.ParamFlags.READWRITE,
            false),
    },
    Signals: {
        'action-added': {param_types: [Action]},
        'action-removed': {param_types: [Action]},
        'activated': {},
        'destroy': {param_types: [GObject.TYPE_UINT]},
    },
}, Notification);
SignalTracker.registerDestroyableType(Notification);

export const MessageTray = GObject.registerClass({
    Signals: {
        'queue-changed': {},
        'source-added': {param_types: [Source.$gtype]},
        'source-removed': {param_types: [Source.$gtype]},
    },
}, class MessageTray extends St.Widget {
    _init() {
        super._init({
            visible: false,
            clip_to_allocation: true,
            layout_manager: new Clutter.BinLayout(),
        });

        this._presence = new GnomeSession.Presence((proxy, _error) => {
            this._onStatusChanged(proxy.status);
        });
        this._busy = false;
        this._bannerBlocked = false;
        this._presence.connectSignal('StatusChanged', (proxy, senderName, [status]) => {
            this._onStatusChanged(status);
        });

        let constraint = new Layout.MonitorConstraint({primary: true});
        Main.layoutManager.panelBox.bind_property('visible',
            constraint, 'work-area',
            GObject.BindingFlags.SYNC_CREATE);
        this.add_constraint(constraint);

        this._bannerBin = new St.Widget({
            name: 'notification-container',
            reactive: true,
            track_hover: true,
            y_align: Clutter.ActorAlign.START,
            x_align: Clutter.ActorAlign.CENTER,
            y_expand: true,
            x_expand: true,
            layout_manager: new Clutter.BinLayout(),
        });
        this._bannerBin.connect('key-release-event',
            this._onNotificationKeyRelease.bind(this));
        this._bannerBin.connect('notify::hover',
            this._onNotificationHoverChanged.bind(this));
        this.add_child(this._bannerBin);

        this._notificationFocusGrabber = new FocusGrabber(this._bannerBin);
        this._notificationQueue = [];
        this._notification = null;
        this._banner = null;

        this._userActiveWhileNotificationShown = false;

        this.idleMonitor = global.backend.get_core_idle_monitor();

        this._useLongerNotificationLeftTimeout = false;

        // pointerInNotification is sort of a misnomer -- it tracks whether
        // a message tray notification should expand. The value is
        // partially driven by the hover state of the notification, but has
        // a lot of complex state related to timeouts and the current
        // state of the pointer when a notification pops up.
        this._pointerInNotification = false;

        // This tracks this._bannerBin.hover and is used to fizzle
        // out non-changing hover notifications in onNotificationHoverChanged.
        this._notificationHovered = false;

        this._notificationState = State.HIDDEN;
        this._notificationTimeoutId = 0;
        this._notificationRemoved = false;

        Main.layoutManager.addChrome(this, {affectsInputRegion: false});
        Main.layoutManager.trackChrome(this._bannerBin, {affectsInputRegion: true});

        global.display.connect('in-fullscreen-changed', this._updateState.bind(this));

        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));

        Main.overview.connect('window-drag-begin',
            this._onDragBegin.bind(this));
        Main.overview.connect('window-drag-cancelled',
            this._onDragEnd.bind(this));
        Main.overview.connect('window-drag-end',
            this._onDragEnd.bind(this));

        Main.overview.connect('item-drag-begin',
            this._onDragBegin.bind(this));
        Main.overview.connect('item-drag-cancelled',
            this._onDragEnd.bind(this));
        Main.overview.connect('item-drag-end',
            this._onDragEnd.bind(this));

        Main.xdndHandler.connect('drag-begin',
            this._onDragBegin.bind(this));
        Main.xdndHandler.connect('drag-end',
            this._onDragEnd.bind(this));

        Main.wm.addKeybinding('focus-active-notification',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.NONE,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._expandActiveNotification.bind(this));

        this._sources = new Set();

        this._sessionUpdated();
    }

    _sessionUpdated() {
        this._updateState();
    }

    _onDragBegin() {
        Shell.util_set_hidden_from_pick(this, true);
    }

    _onDragEnd() {
        Shell.util_set_hidden_from_pick(this, false);
    }

    get bannerAlignment() {
        return this._bannerBin.get_x_align();
    }

    set bannerAlignment(align) {
        this._bannerBin.set_x_align(align);
    }

    _onNotificationKeyRelease(actor, event) {
        if (event.get_key_symbol() === Clutter.KEY_Escape && event.get_state() === 0) {
            this._expireNotification();
            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _expireNotification() {
        this._notificationExpired = true;
        this._updateState();
    }

    get queueCount() {
        return this._notificationQueue.length;
    }

    set bannerBlocked(v) {
        if (this._bannerBlocked === v)
            return;
        this._bannerBlocked = v;
        this._updateState();
    }

    contains(source) {
        return this._sources.has(source);
    }

    add(source) {
        if (this.contains(source)) {
            log(`Trying to re-add source ${source.title}`);
            return;
        }

        // Register that we got a notification for this source
        source.policy.store();

        source.policy.connect('notify::enable', () => {
            this._onSourceEnableChanged(source.policy, source);
        });
        source.policy.connect('notify', this._updateState.bind(this));
        this._onSourceEnableChanged(source.policy, source);
    }

    _addSource(source) {
        this._sources.add(source);

        source.connectObject(
            'notification-request-banner', this._onNotificationRequestBanner.bind(this),
            'notification-removed', this._onNotificationRemoved.bind(this),
            'destroy', () => this._removeSource(source), this);

        this.emit('source-added', source);
    }

    _removeSource(source) {
        this._sources.delete(source);
        source.disconnectObject(this);
        this.emit('source-removed', source);
    }

    getSources() {
        return [...this._sources.keys()];
    }

    _onSourceEnableChanged(policy, source) {
        let wasEnabled = this.contains(source);
        let shouldBeEnabled = policy.enable;

        if (wasEnabled !== shouldBeEnabled) {
            if (shouldBeEnabled)
                this._addSource(source);
            else
                this._removeSource(source);
        }
    }

    _onNotificationRemoved(source, notification) {
        if (this._notification === notification) {
            this._notificationRemoved = true;
            if (this._notificationState === State.SHOWN ||
                this._notificationState === State.SHOWING) {
                this._pointerInNotification = false;
                this._updateNotificationTimeout(0);
                this._updateState();
            }
        } else {
            const index = this._notificationQueue.indexOf(notification);
            if (index !== -1) {
                this._notificationQueue.splice(index, 1);
                this.emit('queue-changed');
            }
        }
    }

    _onNotificationRequestBanner(_source, notification) {
        // We never display a banner for already acknowledged notifications
        if (notification.acknowledged)
            return;

        if (notification.urgency === Urgency.LOW)
            return;

        if (!notification.source.policy.showBanners && notification.urgency !== Urgency.CRITICAL)
            return;

        if (this._notification === notification) {
            // If a notification that is being shown is updated, we update
            // how it is shown and extend the time until it auto-hides.
            // If a new notification is updated while it is being hidden,
            // we stop hiding it and show it again.
            this._updateShowingNotification();
        } else if (!this._notificationQueue.includes(notification)) {
            // If the queue is "full", we skip banner mode and just show a small
            // indicator in the panel; however do make an exception for CRITICAL
            // notifications, as only banner mode allows expansion.
            let bannerCount = this._notification ? 1 : 0;
            let full = this.queueCount + bannerCount >= MAX_NOTIFICATIONS_IN_QUEUE;
            if (!full || notification.urgency === Urgency.CRITICAL) {
                this._notificationQueue.push(notification);
                this._notificationQueue.sort(
                    (n1, n2) => n2.urgency - n1.urgency);
                this.emit('queue-changed');
            }
        }
        this._updateState();
    }

    _resetNotificationLeftTimeout() {
        this._useLongerNotificationLeftTimeout = false;
        if (this._notificationLeftTimeoutId) {
            GLib.source_remove(this._notificationLeftTimeoutId);
            this._notificationLeftTimeoutId = 0;
            this._notificationLeftMouseX = -1;
            this._notificationLeftMouseY = -1;
        }
    }

    _onNotificationHoverChanged() {
        if (this._bannerBin.hover === this._notificationHovered)
            return;

        this._notificationHovered = this._bannerBin.hover;
        if (this._notificationHovered) {
            this._resetNotificationLeftTimeout();

            if (this._showNotificationMouseX >= 0) {
                let actorAtShowNotificationPosition =
                    global.stage.get_actor_at_pos(Clutter.PickMode.ALL, this._showNotificationMouseX, this._showNotificationMouseY);
                this._showNotificationMouseX = -1;
                this._showNotificationMouseY = -1;
                // Don't set this._pointerInNotification to true if the pointer was initially in the area where the notification
                // popped up. That way we will not be expanding notifications that happen to pop up over the pointer
                // automatically. Instead, the user is able to expand the notification by mousing away from it and then
                // mousing back in. Because this is an expected action, we set the boolean flag that indicates that a longer
                // timeout should be used before popping down the notification.
                if (this._bannerBin.contains(actorAtShowNotificationPosition)) {
                    this._useLongerNotificationLeftTimeout = true;
                    return;
                }
            }

            this._pointerInNotification = true;
            this._updateState();
        } else {
            // We record the position of the mouse the moment it leaves the tray. These coordinates are used in
            // this._onNotificationLeftTimeout() to determine if the mouse has moved far enough during the initial timeout for us
            // to consider that the user intended to leave the tray and therefore hide the tray. If the mouse is still
            // close to its previous position, we extend the timeout once.
            let [x, y] = global.get_pointer();
            this._notificationLeftMouseX = x;
            this._notificationLeftMouseY = y;

            // We wait just a little before hiding the message tray in case the user quickly moves the mouse back into it.
            // We wait for a longer period if the notification popped up where the mouse pointer was already positioned.
            // That gives the user more time to mouse away from the notification and mouse back in in order to expand it.
            let timeout = this._useLongerNotificationLeftTimeout ? LONGER_HIDE_TIMEOUT : HIDE_TIMEOUT;
            this._notificationLeftTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout, this._onNotificationLeftTimeout.bind(this));
            GLib.Source.set_name_by_id(this._notificationLeftTimeoutId, '[gnome-shell] this._onNotificationLeftTimeout');
        }
    }

    _onStatusChanged(status) {
        if (status === GnomeSession.PresenceStatus.BUSY) {
            // remove notification and allow the summary to be closed now
            this._updateNotificationTimeout(0);
            this._busy = true;
        } else if (status !== GnomeSession.PresenceStatus.IDLE) {
            // We preserve the previous value of this._busy if the status turns to IDLE
            // so that we don't start showing notifications queued during the BUSY state
            // as the screensaver gets activated.
            this._busy = false;
        }

        this._updateState();
    }

    _onNotificationLeftTimeout() {
        let [x, y] = global.get_pointer();
        // We extend the timeout once if the mouse moved no further than MOUSE_LEFT_ACTOR_THRESHOLD to either side.
        if (this._notificationLeftMouseX > -1 &&
            y < this._notificationLeftMouseY + MOUSE_LEFT_ACTOR_THRESHOLD &&
            y > this._notificationLeftMouseY - MOUSE_LEFT_ACTOR_THRESHOLD &&
            x < this._notificationLeftMouseX + MOUSE_LEFT_ACTOR_THRESHOLD &&
            x > this._notificationLeftMouseX - MOUSE_LEFT_ACTOR_THRESHOLD) {
            this._notificationLeftMouseX = -1;
            this._notificationLeftTimeoutId = GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                LONGER_HIDE_TIMEOUT,
                this._onNotificationLeftTimeout.bind(this));
            GLib.Source.set_name_by_id(this._notificationLeftTimeoutId, '[gnome-shell] this._onNotificationLeftTimeout');
        } else {
            this._notificationLeftTimeoutId = 0;
            this._useLongerNotificationLeftTimeout = false;
            this._pointerInNotification = false;
            this._updateNotificationTimeout(0);
            this._updateState();
        }
        return GLib.SOURCE_REMOVE;
    }

    // All of the logic for what happens when occurs here; the various
    // event handlers merely update variables such as
    // 'this._pointerInNotification', 'this._traySummoned', etc, and
    // _updateState() figures out what (if anything) needs to be done
    // at the present time.
    _updateState() {
        let hasMonitor = Main.layoutManager.primaryMonitor != null;
        this.visible = !this._bannerBlocked && hasMonitor && this._banner != null;
        if (this._bannerBlocked || !hasMonitor)
            return;

        // If our state changes caused _updateState to be called,
        // just exit now to prevent reentrancy issues.
        if (this._updatingState)
            return;

        this._updatingState = true;

        // Filter out acknowledged notifications.
        let changed = false;
        this._notificationQueue = this._notificationQueue.filter(n => {
            changed ||= n.acknowledged;
            return !n.acknowledged;
        });

        if (changed)
            this.emit('queue-changed');

        let hasNotifications = Main.sessionMode.hasNotifications;

        if (this._notificationState === State.HIDDEN) {
            let nextNotification = this._notificationQueue[0] || null;
            if (hasNotifications && nextNotification) {
                let limited = this._busy || Main.layoutManager.primaryMonitor.inFullscreen;
                let showNextNotification = !limited || nextNotification.forFeedback || nextNotification.urgency === Urgency.CRITICAL;
                if (showNextNotification)
                    this._showNotification();
            }
        } else if (this._notificationState === State.SHOWING ||
                   this._notificationState === State.SHOWN) {
            let expired = (this._userActiveWhileNotificationShown &&
                           this._notificationTimeoutId === 0 &&
                           this._notification.urgency !== Urgency.CRITICAL &&
                           !this._pointerInNotification) || this._notificationExpired;
            let mustClose = this._notificationRemoved || !hasNotifications || expired;

            if (mustClose) {
                let animate = hasNotifications && !this._notificationRemoved;
                this._hideNotification(animate);
            } else if (this._notificationState === State.SHOWN &&
                       this._pointerInNotification) {
                if (!this._banner.expanded)
                    this._expandBanner(false);
                else
                    this._ensureBannerFocused();
            }
        }

        this._updatingState = false;

        // Clean transient variables that are used to communicate actions
        // to updateState()
        this._notificationExpired = false;
    }

    _onIdleMonitorBecameActive() {
        this._userActiveWhileNotificationShown = true;
        this._updateNotificationTimeout(2000);
        this._updateState();
    }

    _showNotification() {
        this._notification = this._notificationQueue.shift();
        this.emit('queue-changed');

        this._userActiveWhileNotificationShown = this.idleMonitor.get_idletime() <= IDLE_TIME;
        if (!this._userActiveWhileNotificationShown) {
            // If the user isn't active, set up a watch to let us know
            // when the user becomes active.
            this.idleMonitor.add_user_active_watch(this._onIdleMonitorBecameActive.bind(this));
        }

        this._banner = new Calendar.NotificationMessage(this._notification);
        this._banner.can_focus = false;
        this._banner.add_style_class_name('notification-banner');

        this._bannerBin.add_child(this._banner);

        this._bannerBin.opacity = 0;
        this._bannerBin.y = -this._banner.height;
        this.show();

        Meta.disable_unredirect_for_display(global.display);
        this._updateShowingNotification();

        let [x, y] = global.get_pointer();
        // We save the position of the mouse at the time when we started showing the notification
        // in order to determine if the notification popped up under it. We make that check if
        // the user starts moving the mouse and _onNotificationHoverChanged() gets called. We don't
        // expand the notification if it just happened to pop up under the mouse unless the user
        // explicitly mouses away from it and then mouses back in.
        this._showNotificationMouseX = x;
        this._showNotificationMouseY = y;
        // We save the coordinates of the mouse at the time when we started showing the notification
        // and then we update it in _notificationTimeout(). We don't pop down the notification if
        // the mouse is moving towards it or within it.
        this._lastSeenMouseX = x;
        this._lastSeenMouseY = y;

        this._resetNotificationLeftTimeout();
    }

    _updateShowingNotification() {
        this._notification.acknowledged = true;
        this._notification.playSound();

        // We auto-expand notifications with CRITICAL urgency, or for which the relevant setting
        // is on in the control center.
        if (this._notification.urgency === Urgency.CRITICAL ||
            this._notification.source.policy.forceExpanded)
            this._expandBanner(true);

        // We tween all notifications to full opacity. This ensures that both new notifications and
        // notifications that might have been in the process of hiding get full opacity.
        //
        // We tween any notification showing in the banner mode to the appropriate height
        // (which is banner height or expanded height, depending on the notification state)
        // This ensures that both new notifications and notifications in the banner mode that might
        // have been in the process of hiding are shown with the correct height.
        //
        // We use this._showNotificationCompleted() onComplete callback to extend the time the updated
        // notification is being shown.

        this._notificationState = State.SHOWING;
        this._bannerBin.remove_all_transitions();
        this._bannerBin.ease({
            opacity: 255,
            duration: ANIMATION_TIME,
            mode: Clutter.AnimationMode.LINEAR,
        });
        this._bannerBin.ease({
            y: 0,
            duration: ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_BACK,
            onComplete: () => {
                this._notificationState = State.SHOWN;
                this._showNotificationCompleted();
                this._updateState();
            },
        });
    }

    _showNotificationCompleted() {
        if (this._notification.urgency !== Urgency.CRITICAL)
            this._updateNotificationTimeout(NOTIFICATION_TIMEOUT);
    }

    _updateNotificationTimeout(timeout) {
        if (this._notificationTimeoutId) {
            GLib.source_remove(this._notificationTimeoutId);
            this._notificationTimeoutId = 0;
        }
        if (timeout > 0) {
            this._notificationTimeoutId =
                GLib.timeout_add(GLib.PRIORITY_DEFAULT, timeout,
                    this._notificationTimeout.bind(this));
            GLib.Source.set_name_by_id(this._notificationTimeoutId, '[gnome-shell] this._notificationTimeout');
        }
    }

    _notificationTimeout() {
        let [x, y] = global.get_pointer();
        if (y < this._lastSeenMouseY - 10 && !this._notificationHovered) {
            // The mouse is moving towards the notification, so don't
            // hide it yet. (We just create a new timeout (and destroy
            // the old one) each time because the bookkeeping is
            // simpler.)
            this._updateNotificationTimeout(1000);
        } else if (this._useLongerNotificationLeftTimeout && !this._notificationLeftTimeoutId &&
                  (x !== this._lastSeenMouseX || y !== this._lastSeenMouseY)) {
            // Refresh the timeout if the notification originally
            // popped up under the pointer, and the pointer is hovering
            // inside it.
            this._updateNotificationTimeout(1000);
        } else {
            this._notificationTimeoutId = 0;
            this._updateState();
        }

        this._lastSeenMouseX = x;
        this._lastSeenMouseY = y;
        return GLib.SOURCE_REMOVE;
    }

    _hideNotification(animate) {
        this._notificationFocusGrabber.ungrabFocus();

        this._banner.disconnectObject(this);

        this._resetNotificationLeftTimeout();
        this._bannerBin.remove_all_transitions();

        const duration = animate ? ANIMATION_TIME : 0;
        this._notificationState = State.HIDING;
        this._bannerBin.ease({
            opacity: 0,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_BACK,
        });
        this._bannerBin.ease({
            y: -this._bannerBin.height,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_BACK,
            onComplete: () => {
                this._notificationState = State.HIDDEN;
                this._hideNotificationCompleted();
                this._updateState();
            },
        });
    }

    _hideNotificationCompleted() {
        let notification = this._notification;
        this._notification = null;
        if (!this._notificationRemoved && notification.isTransient)
            notification.destroy(NotificationDestroyedReason.EXPIRED);

        this._pointerInNotification = false;
        this._notificationRemoved = false;
        Meta.enable_unredirect_for_display(global.display);

        this._banner.destroy();
        this._banner = null;
        this.hide();
    }

    _expandActiveNotification() {
        if (!this._banner)
            return;

        this._expandBanner(false);
    }

    _expandBanner(autoExpanding) {
        // Don't animate changes in notifications that are auto-expanding.
        this._banner.expand(!autoExpanding);

        // Don't focus notifications that are auto-expanding.
        if (!autoExpanding)
            this._ensureBannerFocused();
    }

    _ensureBannerFocused() {
        this._notificationFocusGrabber.grabFocus();
    }
});

let systemNotificationSource = null;

/**
 * The {Source} that should be used to send system notifications.
 *
 * @returns {Source}
 */
export function getSystemSource() {
    if (!systemNotificationSource) {
        systemNotificationSource = new Source({
            title: _('System'),
            iconName: 'emblem-system-symbolic',
        });

        systemNotificationSource.connect('destroy', () => {
            systemNotificationSource = null;
        });
        Main.messageTray.add(systemNotificationSource);
    }

    return systemNotificationSource;
}
(uuay)/!credentialManager.js// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import * as Signals from '../misc/signals.js';

export class CredentialManager extends Signals.EventEmitter {
    constructor(service) {
        super();

        this._token = null;
        this._service = service;
    }

    get token() {
        return this._token;
    }

    set token(t) {
        this._token = t;
        if (this._token)
            this.emit('user-authenticated', this._token);
    }

    get service() {
        return this._service;
    }
}
(uuay)lightbox.js�'// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Params from '../misc/params.js';

export const DEFAULT_FADE_FACTOR = 0.4;
export const VIGNETTE_BRIGHTNESS = 0.5;
export const VIGNETTE_SHARPNESS = 0.7;

const VIGNETTE_DECLARATIONS = '                                              \
uniform float brightness;                                                  \n\
uniform float vignette_sharpness;                                          \n\
float rand(vec2 p) {                                                       \n\
  return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453123);        \n\
}                                                                          \n';

const VIGNETTE_CODE = '                                                      \
cogl_color_out.a = cogl_color_in.a;                                        \n\
cogl_color_out.rgb = vec3(0.0, 0.0, 0.0);                                  \n\
vec2 position = cogl_tex_coord_in[0].xy - 0.5;                             \n\
float t = clamp(length(1.41421 * position), 0.0, 1.0);                     \n\
float pixel_brightness = mix(1.0, 1.0 - vignette_sharpness, t);            \n\
cogl_color_out.a *= 1.0 - pixel_brightness * brightness;                   \n\
cogl_color_out.a += (rand(position) - 0.5) / 100.0;                        \n';


const RadialShaderEffect = GObject.registerClass({
    Properties: {
        'brightness': GObject.ParamSpec.float(
            'brightness', 'brightness', 'brightness',
            GObject.ParamFlags.READWRITE,
            0, 1, 1),
        'sharpness': GObject.ParamSpec.float(
            'sharpness', 'sharpness', 'sharpness',
            GObject.ParamFlags.READWRITE,
            0, 1, 0),
    },
}, class RadialShaderEffect extends Shell.GLSLEffect {
    _init(params) {
        this._brightness = undefined;
        this._sharpness = undefined;

        super._init(params);

        this._brightnessLocation = this.get_uniform_location('brightness');
        this._sharpnessLocation = this.get_uniform_location('vignette_sharpness');

        this.brightness = 1.0;
        this.sharpness = 0.0;
    }

    vfunc_build_pipeline() {
        this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT,
            VIGNETTE_DECLARATIONS, VIGNETTE_CODE, true);
    }

    get brightness() {
        return this._brightness;
    }

    set brightness(v) {
        if (this._brightness === v)
            return;
        this._brightness = v;
        this.set_uniform_float(this._brightnessLocation,
            1, [this._brightness]);
        this.notify('brightness');
    }

    get sharpness() {
        return this._sharpness;
    }

    set sharpness(v) {
        if (this._sharpness === v)
            return;
        this._sharpness = v;
        this.set_uniform_float(this._sharpnessLocation,
            1, [this._sharpness]);
        this.notify('sharpness');
    }
});

/**
 * Lightbox:
 */
export const Lightbox = GObject.registerClass({
    Properties: {
        'active': GObject.ParamSpec.boolean(
            'active', 'active', 'active', GObject.ParamFlags.READABLE, false),
    },
}, class Lightbox extends St.Bin {
    /**
     * Lightbox creates a dark translucent "shade" actor to hide the
     * contents of `container`, and allows you to specify particular actors
     * in `container` to highlight by bringing them above the shade. It
     * tracks added and removed actors in `container` while the lightboxing
     * is active, and ensures that all actors are returned to their
     * original stacking order when the lightboxing is removed. (However,
     * if actors are restacked by outside code while the lightboxing is
     * active, the lightbox may later revert them back to their original
     * order.)
     *
     * By default, the shade window will have the height and width of
     * `container` and will track any changes in its size. You can override
     * this by passing an explicit width and height in `params`.
     *
     * @param {Clutter.Container} container parent Clutter.Container
     * @param {object} [params] additional parameters:
     * @param {boolean=} params.inhibitEvents: whether to inhibit events for `container`
     * @param {number=} params.width: shade actor width
     * @param {number=} params.height: shade actor height
     * @param {number=} params.fadeFactor: fading opacity factor
     * @param {boolean=} params.radialEffect: whether to enable the GLSL radial effect
     */
    _init(container, params) {
        params = Params.parse(params, {
            inhibitEvents: false,
            width: null,
            height: null,
            fadeFactor: DEFAULT_FADE_FACTOR,
            radialEffect: false,
        });

        super._init({
            reactive: params.inhibitEvents,
            width: params.width,
            height: params.height,
            visible: false,
        });

        this._active = false;
        this._container = container;
        this._children = container.get_children();
        this._fadeFactor = params.fadeFactor;
        this._radialEffect = params.radialEffect;

        if (this._radialEffect)
            this.add_effect(new RadialShaderEffect({name: 'radial'}));
        else
            this.set({opacity: 0, style_class: 'lightbox'});

        container.add_child(this);
        container.set_child_above_sibling(this, null);

        this.connect('destroy', this._onDestroy.bind(this));

        if (!params.width || !params.height) {
            this.add_constraint(new Clutter.BindConstraint({
                source: container,
                coordinate: Clutter.BindCoordinate.ALL,
            }));
        }

        container.connectObject(
            'child-added', this._childAdded.bind(this),
            'child-removed', this._childRemoved.bind(this), this);

        this._highlighted = null;
    }

    get active() {
        return this._active;
    }

    _childAdded(container, newChild) {
        let children = this._container.get_children();
        let myIndex = children.indexOf(this);
        let newChildIndex = children.indexOf(newChild);

        if (newChildIndex > myIndex) {
            // The child was added above the shade (presumably it was
            // made the new top-most child). Move it below the shade,
            // and add it to this._children as the new topmost actor.
            this._container.set_child_above_sibling(this, newChild);
            this._children.push(newChild);
        } else if (newChildIndex === 0) {
            // Bottom of stack
            this._children.unshift(newChild);
        } else {
            // Somewhere else; insert it into the correct spot
            let prevChild = this._children.indexOf(children[newChildIndex - 1]);
            if (prevChild !== -1) // paranoia
                this._children.splice(prevChild + 1, 0, newChild);
        }
    }

    lightOn(fadeInTime) {
        this.remove_all_transitions();

        let easeProps = {
            duration: fadeInTime || 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        };

        let onComplete = () => {
            this._active = true;
            this.notify('active');
        };

        this.show();

        if (this._radialEffect) {
            this.ease_property(
                '@effects.radial.brightness', VIGNETTE_BRIGHTNESS, easeProps);
            this.ease_property(
                '@effects.radial.sharpness', VIGNETTE_SHARPNESS,
                {onComplete, ...easeProps});
        } else {
            this.ease({
                ...easeProps,
                opacity: 255 * this._fadeFactor,
                onComplete,
            });
        }
    }

    lightOff(fadeOutTime) {
        this.remove_all_transitions();

        this._active = false;
        this.notify('active');

        let easeProps = {
            duration: fadeOutTime || 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        };

        let onComplete = () => this.hide();

        if (this._radialEffect) {
            this.ease_property(
                '@effects.radial.brightness', 1.0, easeProps);
            this.ease_property(
                '@effects.radial.sharpness', 0.0, {onComplete, ...easeProps});
        } else {
            this.ease({...easeProps, opacity: 0, onComplete});
        }
    }

    _childRemoved(container, child) {
        let index = this._children.indexOf(child);
        if (index !== -1) // paranoia
            this._children.splice(index, 1);

        if (child === this._highlighted)
            this._highlighted = null;
    }

    /**
     * highlight:
     *
     * Highlights the indicated actor and unhighlights any other
     * currently-highlighted actor. With no arguments or a false/null
     * argument, all actors will be unhighlighted.
     *
     * @param {Clutter.Actor=} window actor to highlight
     */
    highlight(window) {
        if (this._highlighted === window)
            return;

        // Walk this._children raising and lowering actors as needed.
        // Things get a little tricky if the to-be-raised and
        // to-be-lowered actors were originally adjacent, in which
        // case we may need to indicate some *other* actor as the new
        // sibling of the to-be-lowered one.

        let below = this;
        for (let i = this._children.length - 1; i >= 0; i--) {
            if (this._children[i] === window)
                this._container.set_child_above_sibling(this._children[i], null);
            else if (this._children[i] === this._highlighted)
                this._container.set_child_below_sibling(this._children[i], below);
            else
                below = this._children[i];
        }

        this._highlighted = window;
    }

    /**
     * _onDestroy:
     *
     * This is called when the lightbox' actor is destroyed, either
     * by destroying its container or by explicitly calling this.destroy().
     */
    _onDestroy() {
        this.highlight(null);
    }
});
(uuay)altTab.js��// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import St from 'gi://St';
import Shell from 'gi://Shell';

import * as Main from './main.js';
import * as SwitcherPopup from './switcherPopup.js';

const APP_ICON_HOVER_TIMEOUT = 200; // milliseconds

const THUMBNAIL_DEFAULT_SIZE = 256;
const THUMBNAIL_POPUP_TIME = 500; // milliseconds
const THUMBNAIL_FADE_TIME = 100; // milliseconds

const WINDOW_PREVIEW_SIZE = 128;
const APP_ICON_SIZE = 96;
const APP_ICON_SIZE_SMALL = 48;

const baseIconSizes = [96, 64, 48, 32, 22];

/** @enum {number} */
const AppIconMode = {
    THUMBNAIL_ONLY: 1,
    APP_ICON_ONLY: 2,
    BOTH: 3,
};

function _createWindowClone(window, size) {
    let [width, height] = window.get_size();
    let scale = Math.min(1.0, size / width, size / height);
    return new Clutter.Clone({
        source: window,
        width: width * scale,
        height: height * scale,
        x_align: Clutter.ActorAlign.CENTER,
        y_align: Clutter.ActorAlign.CENTER,
        // usual hack for the usual bug in ClutterBinLayout...
        x_expand: true,
        y_expand: true,
    });
}

/**
 * @param {Meta.Workspace} workspace
 * @returns {Meta.Window}
 */
function getWindows(workspace) {
    // We ignore skip-taskbar windows in switchers, but if they are attached
    // to their parent, their position in the MRU list may be more appropriate
    // than the parent; so start with the complete list ...
    let windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, workspace);
    // ... map windows to their parent where appropriate ...
    return windows.map(w => {
        return w.is_attached_dialog() ? w.get_transient_for() : w;
    // ... and filter out skip-taskbar windows and duplicates
    }).filter((w, i, a) => !w.skip_taskbar && a.indexOf(w) === i);
}

export const AppSwitcherPopup = GObject.registerClass(
class AppSwitcherPopup extends SwitcherPopup.SwitcherPopup {
    _init() {
        super._init();

        this._thumbnails = null;
        this._thumbnailTimeoutId = 0;
        this._currentWindow = -1;

        this.thumbnailsVisible = false;

        let apps = Shell.AppSystem.get_default().get_running();

        this._switcherList = new AppSwitcher(apps, this);
        this._items = this._switcherList.icons;
    }

    vfunc_allocate(box) {
        super.vfunc_allocate(box);

        // Allocate the thumbnails
        // We try to avoid overflowing the screen so we base the resulting size on
        // those calculations
        if (this._thumbnails) {
            let childBox = this._switcherList.get_allocation_box();
            let primary = Main.layoutManager.primaryMonitor;

            let leftPadding = this.get_theme_node().get_padding(St.Side.LEFT);
            let rightPadding = this.get_theme_node().get_padding(St.Side.RIGHT);
            let bottomPadding = this.get_theme_node().get_padding(St.Side.BOTTOM);
            let hPadding = leftPadding + rightPadding;

            let icon = this._items[this._selectedIndex];
            let [posX] = icon.get_transformed_position();
            let thumbnailCenter = posX + icon.width / 2;
            let [, childNaturalWidth] = this._thumbnails.get_preferred_width(-1);
            childBox.x1 = Math.max(primary.x + leftPadding, Math.floor(thumbnailCenter - childNaturalWidth / 2));
            if (childBox.x1 + childNaturalWidth > primary.x + primary.width - hPadding) {
                let offset = childBox.x1 + childNaturalWidth - primary.width + hPadding;
                childBox.x1 = Math.max(primary.x + leftPadding, childBox.x1 - offset - hPadding);
            }

            let spacing = this.get_theme_node().get_length('spacing');

            childBox.x2 = childBox.x1 +  childNaturalWidth;
            if (childBox.x2 > primary.x + primary.width - rightPadding)
                childBox.x2 = primary.x + primary.width - rightPadding;
            childBox.y1 = this._switcherList.allocation.y2 + spacing;
            this._thumbnails.addClones(primary.y + primary.height - bottomPadding - childBox.y1);
            let [, childNaturalHeight] = this._thumbnails.get_preferred_height(-1);
            childBox.y2 = childBox.y1 + childNaturalHeight;
            this._thumbnails.allocate(childBox);
        }
    }

    _initialSelection(backward, binding) {
        if (binding === 'switch-group') {
            if (backward)
                this._select(0, this._items[0].cachedWindows.length - 1);
            else if (this._items[0].cachedWindows.length > 1)
                this._select(0, 1);
            else
                this._select(0, 0);
        } else if (binding === 'switch-group-backward') {
            this._select(0, this._items[0].cachedWindows.length - 1);
        } else if (binding === 'switch-applications-backward') {
            this._select(this._items.length - 1);
        } else if (this._items.length === 1) {
            this._select(0);
        } else if (backward) {
            this._select(this._items.length - 1);
        } else {
            this._select(1);
        }
    }

    _nextWindow() {
        // We actually want the second window if we're in the unset state
        if (this._currentWindow === -1)
            this._currentWindow = 0;
        return SwitcherPopup.mod(
            this._currentWindow + 1,
            this._items[this._selectedIndex].cachedWindows.length);
    }

    _previousWindow() {
        // Also assume second window here
        if (this._currentWindow === -1)
            this._currentWindow = 1;
        return SwitcherPopup.mod(
            this._currentWindow - 1,
            this._items[this._selectedIndex].cachedWindows.length);
    }

    _closeAppWindow(appIndex, windowIndex) {
        let appIcon = this._items[appIndex];
        if (!appIcon)
            return;

        let window = appIcon.cachedWindows[windowIndex];
        if (!window)
            return;

        window.delete(global.get_current_time());
    }

    _quitApplication(appIndex) {
        let appIcon = this._items[appIndex];
        if (!appIcon)
            return;

        appIcon.app.request_quit();
    }

    _keyPressHandler(keysym, action) {
        const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
        if (action === Meta.KeyBindingAction.SWITCH_GROUP) {
            if (!this._thumbnailsFocused)
                this._select(this._selectedIndex, 0);
            else
                this._select(this._selectedIndex, this._nextWindow());
        } else if (action === Meta.KeyBindingAction.SWITCH_GROUP_BACKWARD) {
            this._select(this._selectedIndex, this._previousWindow());
        } else if (action === Meta.KeyBindingAction.SWITCH_APPLICATIONS) {
            this._select(this._next());
        } else if (action === Meta.KeyBindingAction.SWITCH_APPLICATIONS_BACKWARD) {
            this._select(this._previous());
        } else if (keysym === Clutter.KEY_q || keysym === Clutter.KEY_Q) {
            this._quitApplication(this._selectedIndex);
        } else if (this._thumbnailsFocused) {
            if (keysym === Clutter.KEY_Left)
                this._select(this._selectedIndex, rtl ? this._nextWindow() : this._previousWindow());
            else if (keysym === Clutter.KEY_Right)
                this._select(this._selectedIndex, rtl ? this._previousWindow() : this._nextWindow());
            else if (keysym === Clutter.KEY_Up)
                this._select(this._selectedIndex, null, true);
            else if (keysym === Clutter.KEY_w || keysym === Clutter.KEY_W || keysym === Clutter.KEY_F4)
                this._closeAppWindow(this._selectedIndex, this._currentWindow);
            else
                return Clutter.EVENT_PROPAGATE;
        } else if (keysym === Clutter.KEY_Left) {
            this._select(rtl ? this._next() : this._previous());
        } else if (keysym === Clutter.KEY_Right) {
            this._select(rtl ? this._previous() : this._next());
        } else if (keysym === Clutter.KEY_Down) {
            this._select(this._selectedIndex, 0);
        } else {
            return Clutter.EVENT_PROPAGATE;
        }

        return Clutter.EVENT_STOP;
    }

    _scrollHandler(direction) {
        if (direction === Clutter.ScrollDirection.UP) {
            if (this._thumbnailsFocused) {
                if (this._currentWindow === 0 || this._currentWindow === -1)
                    this._select(this._previous());
                else
                    this._select(this._selectedIndex, this._previousWindow());
            } else {
                let nwindows = this._items[this._selectedIndex].cachedWindows.length;
                if (nwindows > 1)
                    this._select(this._selectedIndex, nwindows - 1);
                else
                    this._select(this._previous());
            }
        } else if (direction === Clutter.ScrollDirection.DOWN) {
            if (this._thumbnailsFocused) {
                if (this._currentWindow === this._items[this._selectedIndex].cachedWindows.length - 1)
                    this._select(this._next());
                else
                    this._select(this._selectedIndex, this._nextWindow());
            } else {
                let nwindows = this._items[this._selectedIndex].cachedWindows.length;
                if (nwindows > 1)
                    this._select(this._selectedIndex, 0);
                else
                    this._select(this._next());
            }
        }
    }

    _itemActivatedHandler(n) {
        // If the user clicks on the selected app, activate the
        // selected window; otherwise (eg, they click on an app while
        // !mouseActive) activate the clicked-on app.
        if (n === this._selectedIndex && this._currentWindow >= 0)
            this._select(n, this._currentWindow);
        else
            this._select(n);
    }

    _windowActivated(thumbnailSwitcher, n) {
        let appIcon = this._items[this._selectedIndex];
        Main.activateWindow(appIcon.cachedWindows[n]);
        this.fadeAndDestroy();
    }

    _windowEntered(thumbnailSwitcher, n) {
        if (!this.mouseActive)
            return;

        this._select(this._selectedIndex, n);
    }

    _windowRemoved(thumbnailSwitcher, n) {
        let appIcon = this._items[this._selectedIndex];
        if (!appIcon)
            return;

        if (appIcon.cachedWindows.length > 0) {
            let newIndex = Math.min(n, appIcon.cachedWindows.length - 1);
            this._select(this._selectedIndex, newIndex);
        }
    }

    _finish(timestamp) {
        let appIcon = this._items[this._selectedIndex];
        if (this._currentWindow < 0)
            appIcon.app.activate_window(appIcon.cachedWindows[0], timestamp);
        else if (appIcon.cachedWindows[this._currentWindow])
            Main.activateWindow(appIcon.cachedWindows[this._currentWindow], timestamp);

        super._finish(timestamp);
    }

    _onDestroy() {
        if (this._thumbnailTimeoutId !== 0)
            GLib.source_remove(this._thumbnailTimeoutId);

        super._onDestroy();
    }

    /**
     * _select:
     *
     * @param {number} app index of the app to select
     * @param {number} [window] index of which of `app`'s windows to select
     * @param {boolean} [forceAppFocus] optional flag, see below
     *
     * Selects the indicated `app`, and optional `window`, and sets
     * this._thumbnailsFocused appropriately to indicate whether the
     * arrow keys should act on the app list or the thumbnail list.
     *
     * If `app` is specified and `window` is unspecified or %null, then
     * the app is highlighted (ie, given a light background), and the
     * current thumbnail list, if any, is destroyed. If `app` has
     * multiple windows, and `forceAppFocus` is not %true, then a
     * timeout is started to open a thumbnail list.
     *
     * If `app` and `window` are specified (and `forceAppFocus` is not),
     * then `app` will be outlined, a thumbnail list will be created
     * and focused (if it hasn't been already), and the `window`'th
     * window in it will be highlighted.
     *
     * If `app` and `window` are specified and `forceAppFocus` is %true,
     * then `app` will be highlighted, and `window` outlined, and the
     * app list will have the keyboard focus.
     */
    _select(app, window, forceAppFocus) {
        if (app !== this._selectedIndex || window == null) {
            if (this._thumbnails)
                this._destroyThumbnails();
        }

        if (this._thumbnailTimeoutId !== 0) {
            GLib.source_remove(this._thumbnailTimeoutId);
            this._thumbnailTimeoutId = 0;
        }

        this._thumbnailsFocused = (window != null) && !forceAppFocus;

        this._selectedIndex = app;
        this._currentWindow = window ? window : -1;
        this._switcherList.highlight(app, this._thumbnailsFocused);

        if (window != null) {
            if (!this._thumbnails)
                this._createThumbnails();
            this._currentWindow = window;
            this._thumbnails.highlight(window, forceAppFocus);
        } else if (this._items[this._selectedIndex].cachedWindows.length > 1 &&
                   !forceAppFocus) {
            this._thumbnailTimeoutId = GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                THUMBNAIL_POPUP_TIME,
                this._timeoutPopupThumbnails.bind(this));
            GLib.Source.set_name_by_id(this._thumbnailTimeoutId, '[gnome-shell] this._timeoutPopupThumbnails');
        }
    }

    _timeoutPopupThumbnails() {
        if (!this._thumbnails)
            this._createThumbnails();
        this._thumbnailTimeoutId = 0;
        this._thumbnailsFocused = false;
        return GLib.SOURCE_REMOVE;
    }

    _destroyThumbnails() {
        let thumbnailsActor = this._thumbnails;
        this._thumbnails.ease({
            opacity: 0,
            duration: THUMBNAIL_FADE_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                thumbnailsActor.destroy();
                this.thumbnailsVisible = false;
            },
        });
        this._thumbnails = null;
        this._switcherList.removeAccessibleState(this._selectedIndex, Atk.StateType.EXPANDED);
    }

    _createThumbnails() {
        this._thumbnails = new ThumbnailSwitcher(this._items[this._selectedIndex].cachedWindows);
        this._thumbnails.connect('item-activated', this._windowActivated.bind(this));
        this._thumbnails.connect('item-entered', this._windowEntered.bind(this));
        this._thumbnails.connect('item-removed', this._windowRemoved.bind(this));
        this._thumbnails.connect('destroy', () => {
            this._thumbnails = null;
            this._thumbnailsFocused = false;
        });

        this.add_child(this._thumbnails);

        // Need to force an allocation so we can figure out whether we
        // need to scroll when selecting
        this._thumbnails.get_allocation_box();

        this._thumbnails.opacity = 0;
        this._thumbnails.ease({
            opacity: 255,
            duration: THUMBNAIL_FADE_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this.thumbnailsVisible = true;
            },
        });

        this._switcherList.addAccessibleState(this._selectedIndex, Atk.StateType.EXPANDED);
    }
});

const CyclerHighlight = GObject.registerClass(
class CyclerHighlight extends St.Widget {
    _init() {
        super._init({layout_manager: new Clutter.BinLayout()});
        this._window = null;

        this._clone = new Clutter.Clone();
        this.add_child(this._clone);

        this._highlight = new St.Widget({style_class: 'cycler-highlight'});
        this.add_child(this._highlight);

        let coordinate = Clutter.BindCoordinate.ALL;
        let constraint = new Clutter.BindConstraint({coordinate});
        this._clone.bind_property('source', constraint, 'source', 0);

        this.add_constraint(constraint);

        this.connect('destroy', this._onDestroy.bind(this));
    }

    set window(w) {
        if (this._window === w)
            return;

        this._window?.disconnectObject(this);

        this._window = w;

        if (this._clone.source)
            this._clone.source.sync_visibility();

        const windowActor = this._window?.get_compositor_private() ?? null;

        if (windowActor)
            windowActor.hide();

        this._clone.source = windowActor;

        if (this._window) {
            this._onSizeChanged();
            this._window.connectObject('size-changed',
                this._onSizeChanged.bind(this), this);
        } else {
            this._highlight.set_size(0, 0);
            this._highlight.hide();
        }
    }

    _onSizeChanged() {
        const bufferRect = this._window.get_buffer_rect();
        const rect = this._window.get_frame_rect();
        this._highlight.set_size(rect.width, rect.height);
        this._highlight.set_position(
            rect.x - bufferRect.x,
            rect.y - bufferRect.y);
        this._highlight.show();
    }

    _onDestroy() {
        this.window = null;
    }
});

// We don't show an actual popup, so just provide what SwitcherPopup
// expects instead of inheriting from SwitcherList
const CyclerList = GObject.registerClass({
    Signals: {
        'item-activated': {param_types: [GObject.TYPE_INT]},
        'item-entered': {param_types: [GObject.TYPE_INT]},
        'item-removed': {param_types: [GObject.TYPE_INT]},
        'item-highlighted': {param_types: [GObject.TYPE_INT]},
    },
}, class CyclerList extends St.Widget {
    highlight(index, _justOutline) {
        this.emit('item-highlighted', index);
    }
});

const CyclerPopup = GObject.registerClass({
    GTypeFlags: GObject.TypeFlags.ABSTRACT,
}, class CyclerPopup extends SwitcherPopup.SwitcherPopup {
    _init() {
        super._init();

        this._items = this._getWindows();

        this._highlight = new CyclerHighlight();
        global.window_group.add_child(this._highlight);

        this._switcherList = new CyclerList();
        this._switcherList.connect('item-highlighted', (list, index) => {
            this._highlightItem(index);
        });
    }

    _highlightItem(index, _justOutline) {
        this._highlight.window = this._items[index];
        global.window_group.set_child_above_sibling(this._highlight, null);
    }

    _finish() {
        let window = this._items[this._selectedIndex];
        let ws = window.get_workspace();
        let workspaceManager = global.workspace_manager;
        let activeWs = workspaceManager.get_active_workspace();

        if (window.minimized) {
            Main.wm.skipNextEffect(window.get_compositor_private());
            window.unminimize();
        }

        if (activeWs === ws) {
            Main.activateWindow(window);
        } else {
            // If the selected window is on a different workspace, we don't
            // want it to disappear, then slide in with the workspace; instead,
            // always activate it on the active workspace ...
            activeWs.activate_with_focus(window, global.get_current_time());

            // ... then slide it over to the original workspace if necessary
            Main.wm.actionMoveWindow(window, ws);
        }

        super._finish();
    }

    _onDestroy() {
        this._highlight.destroy();

        super._onDestroy();
    }
});


export const GroupCyclerPopup = GObject.registerClass(
class GroupCyclerPopup extends CyclerPopup {
    _init() {
        this._settings = new Gio.Settings({schema_id: 'org.gnome.shell.app-switcher'});
        super._init();
    }

    _getWindows() {
        let app = Shell.WindowTracker.get_default().focus_app;
        let appWindows = app?.get_windows() ?? [];

        if (this._settings.get_boolean('current-workspace-only')) {
            const workspaceManager = global.workspace_manager;
            const workspace = workspaceManager.get_active_workspace();
            appWindows = appWindows.filter(
                window => window.located_on_workspace(workspace));
        }

        return appWindows;
    }

    _keyPressHandler(keysym, action) {
        if (action === Meta.KeyBindingAction.CYCLE_GROUP)
            this._select(this._next());
        else if (action === Meta.KeyBindingAction.CYCLE_GROUP_BACKWARD)
            this._select(this._previous());
        else
            return Clutter.EVENT_PROPAGATE;

        return Clutter.EVENT_STOP;
    }
});

export const WindowSwitcherPopup = GObject.registerClass(
class WindowSwitcherPopup extends SwitcherPopup.SwitcherPopup {
    _init() {
        super._init();
        this._settings = new Gio.Settings({schema_id: 'org.gnome.shell.window-switcher'});

        let windows = this._getWindowList();

        let mode = this._settings.get_enum('app-icon-mode');
        this._switcherList = new WindowSwitcher(windows, mode);
        this._items = this._switcherList.icons;
    }

    _getWindowList() {
        let workspace = null;

        if (this._settings.get_boolean('current-workspace-only')) {
            let workspaceManager = global.workspace_manager;

            workspace = workspaceManager.get_active_workspace();
        }

        return getWindows(workspace);
    }

    _closeWindow(windowIndex) {
        let windowIcon = this._items[windowIndex];
        if (!windowIcon)
            return;

        windowIcon.window.delete(global.get_current_time());
    }

    _keyPressHandler(keysym, action) {
        const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
        if (action === Meta.KeyBindingAction.SWITCH_WINDOWS)
            this._select(this._next());
        else if (action === Meta.KeyBindingAction.SWITCH_WINDOWS_BACKWARD)
            this._select(this._previous());
        else if (keysym === Clutter.KEY_Left)
            this._select(rtl ? this._next() : this._previous());
        else if (keysym === Clutter.KEY_Right)
            this._select(rtl ? this._previous() : this._next());
        else if (keysym === Clutter.KEY_w || keysym === Clutter.KEY_W || keysym === Clutter.KEY_F4)
            this._closeWindow(this._selectedIndex);
        else
            return Clutter.EVENT_PROPAGATE;

        return Clutter.EVENT_STOP;
    }

    _finish() {
        Main.activateWindow(this._items[this._selectedIndex].window);

        super._finish();
    }
});

export const WindowCyclerPopup = GObject.registerClass(
class WindowCyclerPopup extends CyclerPopup {
    _init() {
        this._settings = new Gio.Settings({schema_id: 'org.gnome.shell.window-switcher'});
        super._init();
    }

    _getWindows() {
        let workspace = null;

        if (this._settings.get_boolean('current-workspace-only')) {
            let workspaceManager = global.workspace_manager;

            workspace = workspaceManager.get_active_workspace();
        }

        return getWindows(workspace);
    }

    _keyPressHandler(keysym, action) {
        if (action === Meta.KeyBindingAction.CYCLE_WINDOWS)
            this._select(this._next());
        else if (action === Meta.KeyBindingAction.CYCLE_WINDOWS_BACKWARD)
            this._select(this._previous());
        else
            return Clutter.EVENT_PROPAGATE;

        return Clutter.EVENT_STOP;
    }
});

export const AppIcon = GObject.registerClass(
class AppIcon extends St.BoxLayout {
    _init(app) {
        super._init({
            style_class: 'alt-tab-app',
            vertical: true,
        });

        this.app = app;
        this.icon = null;
        this._iconBin = new St.Bin();

        this.add_child(this._iconBin);
        this.label = new St.Label({
            text: this.app.get_name(),
            x_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this.label);
    }

    // eslint-disable-next-line camelcase
    set_size(size) {
        this.icon = this.app.create_icon_texture(size);
        this._iconBin.child = this.icon;
    }
});

const AppSwitcher = GObject.registerClass(
class AppSwitcher extends SwitcherPopup.SwitcherList {
    _init(apps, altTabPopup) {
        super._init(true);

        this.icons = [];
        this._arrows = [];

        let windowTracker = Shell.WindowTracker.get_default();
        let settings = new Gio.Settings({schema_id: 'org.gnome.shell.app-switcher'});

        let workspace = null;
        if (settings.get_boolean('current-workspace-only')) {
            let workspaceManager = global.workspace_manager;

            workspace = workspaceManager.get_active_workspace();
        }

        let allWindows = getWindows(workspace);

        // Construct the AppIcons, add to the popup
        for (let i = 0; i < apps.length; i++) {
            let appIcon = new AppIcon(apps[i]);
            // Cache the window list now; we don't handle dynamic changes here,
            // and we don't want to be continually retrieving it
            appIcon.cachedWindows = allWindows.filter(
                w => windowTracker.get_window_app(w) === appIcon.app);
            if (appIcon.cachedWindows.length > 0)
                this._addIcon(appIcon);
        }

        this._altTabPopup = altTabPopup;
        this._delayedHighlighted = -1;
        this._mouseTimeOutId = 0;

        this.connect('destroy', this._onDestroy.bind(this));
    }

    _onDestroy() {
        if (this._mouseTimeOutId !== 0)
            GLib.source_remove(this._mouseTimeOutId);

        this.icons.forEach(
            icon => icon.app.disconnectObject(this));
    }

    _setIconSize() {
        let j = 0;
        while (this._items.length > 1 && this._items[j].style_class !== 'item-box')
            j++;

        let themeNode = this._items[j].get_theme_node();
        this._list.ensure_style();

        let iconPadding = themeNode.get_horizontal_padding();
        let iconBorder = themeNode.get_border_width(St.Side.LEFT) + themeNode.get_border_width(St.Side.RIGHT);
        let [, labelNaturalHeight] = this.icons[j].label.get_preferred_height(-1);
        let iconSpacing = labelNaturalHeight + iconPadding + iconBorder;
        let totalSpacing = this._list.spacing * (this._items.length - 1);

        // We just assume the whole screen here due to weirdness happening with the passed width
        let primary = Main.layoutManager.primaryMonitor;
        let parentPadding = this.get_parent().get_theme_node().get_horizontal_padding();
        let availWidth = primary.width - parentPadding - this.get_theme_node().get_horizontal_padding();

        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        let iconSizes = baseIconSizes.map(s => s * scaleFactor);
        let iconSize = baseIconSizes[0];

        if (this._items.length > 1) {
            for (let i =  0; i < baseIconSizes.length; i++) {
                iconSize = baseIconSizes[i];
                let height = iconSizes[i] + iconSpacing;
                let w = height * this._items.length + totalSpacing;
                if (w <= availWidth)
                    break;
            }
        }

        this._iconSize = iconSize;

        for (let i = 0; i < this.icons.length; i++) {
            if (this.icons[i].icon != null)
                break;
            this.icons[i].set_size(iconSize);
        }
    }

    vfunc_get_preferred_height(forWidth) {
        if (!this._iconSize)
            this._setIconSize();

        return super.vfunc_get_preferred_height(forWidth);
    }

    vfunc_allocate(box) {
        // Allocate the main list items
        super.vfunc_allocate(box);

        let contentBox = this.get_theme_node().get_content_box(box);

        let arrowHeight = Math.floor(this.get_theme_node().get_padding(St.Side.BOTTOM) / 3);
        let arrowWidth = arrowHeight * 2;

        // Now allocate each arrow underneath its item
        let childBox = new Clutter.ActorBox();
        for (let i = 0; i < this._items.length; i++) {
            let itemBox = this._items[i].allocation;
            childBox.x1 = contentBox.x1 + Math.floor(itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2);
            childBox.x2 = childBox.x1 + arrowWidth;
            childBox.y1 = contentBox.y1 + itemBox.y2 + arrowHeight;
            childBox.y2 = childBox.y1 + arrowHeight;
            this._arrows[i].allocate(childBox);
        }
    }

    // We override SwitcherList's _onItemMotion method to delay
    // activation when the thumbnail list is open
    _onItemMotion(item) {
        if (item === this._items[this._highlighted] ||
            item === this._items[this._delayedHighlighted])
            return Clutter.EVENT_PROPAGATE;

        const index = this._items.indexOf(item);

        if (this._mouseTimeOutId !== 0) {
            GLib.source_remove(this._mouseTimeOutId);
            this._delayedHighlighted = -1;
            this._mouseTimeOutId = 0;
        }

        if (this._altTabPopup.thumbnailsVisible) {
            this._delayedHighlighted = index;
            this._mouseTimeOutId = GLib.timeout_add(
                GLib.PRIORITY_DEFAULT,
                APP_ICON_HOVER_TIMEOUT,
                () => {
                    this._enterItem(index);
                    this._delayedHighlighted = -1;
                    this._mouseTimeOutId = 0;
                    return GLib.SOURCE_REMOVE;
                });
            GLib.Source.set_name_by_id(this._mouseTimeOutId, '[gnome-shell] this._enterItem');
        } else {
            this._itemEntered(index);
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _enterItem(index) {
        let [x, y] = global.get_pointer();
        let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);
        if (this._items[index].contains(pickedActor))
            this._itemEntered(index);
    }

    // We override SwitcherList's highlight() method to also deal with
    // the AppSwitcher->ThumbnailSwitcher arrows. Apps with only 1 window
    // will hide their arrows by default, but show them when their
    // thumbnails are visible (ie, when the app icon is supposed to be
    // in justOutline mode). Apps with multiple windows will normally
    // show a dim arrow, but show a bright arrow when they are
    // highlighted.
    highlight(n, justOutline) {
        if (this.icons[this._highlighted]) {
            if (this.icons[this._highlighted].cachedWindows.length === 1)
                this._arrows[this._highlighted].hide();
            else
                this._arrows[this._highlighted].remove_style_pseudo_class('highlighted');
        }

        super.highlight(n, justOutline);

        if (this._highlighted !== -1) {
            if (justOutline && this.icons[this._highlighted].cachedWindows.length === 1)
                this._arrows[this._highlighted].show();
            else
                this._arrows[this._highlighted].add_style_pseudo_class('highlighted');
        }
    }

    _addIcon(appIcon) {
        this.icons.push(appIcon);
        let item = this.addItem(appIcon, appIcon.label);

        appIcon.app.connectObject('notify::state', app => {
            if (app.state !== Shell.AppState.RUNNING)
                this._removeIcon(app);
        }, this);

        let arrow = new St.DrawingArea({style_class: 'switcher-arrow'});
        arrow.connect('repaint', () => SwitcherPopup.drawArrow(arrow, St.Side.BOTTOM));
        this.add_child(arrow);
        this._arrows.push(arrow);

        if (appIcon.cachedWindows.length === 1)
            arrow.hide();
        else
            item.add_accessible_state(Atk.StateType.EXPANDABLE);
    }

    _removeIcon(app) {
        let index = this.icons.findIndex(icon => {
            return icon.app === app;
        });
        if (index === -1)
            return;

        this._arrows[index].destroy();
        this._arrows.splice(index, 1);

        this.icons.splice(index, 1);
        this.removeItem(index);
    }
});

const ThumbnailSwitcher = GObject.registerClass(
class ThumbnailSwitcher extends SwitcherPopup.SwitcherList {
    _init(windows) {
        super._init(false);

        this._labels = [];
        this._thumbnailBins = [];
        this._clones = [];
        this._windows = windows;

        for (let i = 0; i < windows.length; i++) {
            const box = new St.BoxLayout({
                style_class: 'thumbnail-box',
                vertical: true,
            });

            let bin = new St.Bin({style_class: 'thumbnail'});

            box.add_child(bin);
            this._thumbnailBins.push(bin);

            const title = windows[i].get_title();
            const name = new St.Label({
                text: title,
                // St.Label doesn't support text-align
                x_align: Clutter.ActorAlign.CENTER,
            });
            this._labels.push(name);
            box.add_child(name);

            this.addItem(box, name);
        }

        this.connect('destroy', this._onDestroy.bind(this));
    }

    addClones(availHeight) {
        if (!this._thumbnailBins.length)
            return;
        let totalPadding = this._items[0].get_theme_node().get_horizontal_padding() + this._items[0].get_theme_node().get_vertical_padding();
        totalPadding += this.get_theme_node().get_horizontal_padding() + this.get_theme_node().get_vertical_padding();
        let [, labelNaturalHeight] = this._labels[0].get_preferred_height(-1);
        let spacing = this._items[0].child.get_theme_node().get_length('spacing');
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        let thumbnailSize = THUMBNAIL_DEFAULT_SIZE * scaleFactor;

        availHeight = Math.min(availHeight - labelNaturalHeight - totalPadding - spacing, thumbnailSize);
        let binHeight = availHeight + this._items[0].get_theme_node().get_vertical_padding() + this.get_theme_node().get_vertical_padding() - spacing;
        binHeight = Math.min(thumbnailSize, binHeight);

        for (let i = 0; i < this._thumbnailBins.length; i++) {
            let mutterWindow = this._windows[i].get_compositor_private();
            if (!mutterWindow)
                continue;

            let clone = _createWindowClone(mutterWindow, thumbnailSize);
            this._thumbnailBins[i].set_height(binHeight);
            this._thumbnailBins[i].child = clone;

            mutterWindow.connectObject('destroy',
                source => this._removeThumbnail(source, clone), this);
            this._clones.push(clone);
        }

        // Make sure we only do this once
        this._thumbnailBins = [];
    }

    _removeThumbnail(source, clone) {
        let index = this._clones.indexOf(clone);
        if (index === -1)
            return;

        this._clones.splice(index, 1);
        this._windows.splice(index, 1);
        this._labels.splice(index, 1);
        this.removeItem(index);

        if (this._clones.length > 0)
            this.highlight(SwitcherPopup.mod(index, this._clones.length));
        else
            this.destroy();
    }

    _onDestroy() {
        this._clones.forEach(
            clone => clone?.source.disconnectObject(this));
    }
});

export const WindowIcon = GObject.registerClass(
class WindowIcon extends St.BoxLayout {
    _init(window, mode) {
        super._init({
            style_class: 'alt-tab-app',
            vertical: true,
        });

        this.window = window;

        this._icon = new St.Widget({layout_manager: new Clutter.BinLayout()});

        this.add_child(this._icon);
        this.label = new St.Label({text: window.get_title()});

        let tracker = Shell.WindowTracker.get_default();
        this.app = tracker.get_window_app(window);

        let mutterWindow = this.window.get_compositor_private();
        let size;

        this._icon.destroy_all_children();

        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;

        switch (mode) {
        case AppIconMode.THUMBNAIL_ONLY:
            size = WINDOW_PREVIEW_SIZE;
            this._icon.add_child(_createWindowClone(mutterWindow, size * scaleFactor));
            break;

        case AppIconMode.BOTH:
            size = WINDOW_PREVIEW_SIZE;
            this._icon.add_child(_createWindowClone(mutterWindow, size * scaleFactor));

            if (this.app) {
                this._icon.add_child(
                    this._createAppIcon(this.app, APP_ICON_SIZE_SMALL));
            }
            break;

        case AppIconMode.APP_ICON_ONLY:
            size = APP_ICON_SIZE;
            this._icon.add_child(this._createAppIcon(this.app, size));
        }

        this._icon.set_size(size * scaleFactor, size * scaleFactor);
    }

    _createAppIcon(app, size) {
        let appIcon = app
            ? app.create_icon_texture(size)
            : new St.Icon({icon_name: 'icon-missing', icon_size: size});
        appIcon.x_expand = appIcon.y_expand = true;
        appIcon.x_align = appIcon.y_align = Clutter.ActorAlign.END;

        return appIcon;
    }
});

const WindowSwitcher = GObject.registerClass(
class WindowSwitcher extends SwitcherPopup.SwitcherList {
    _init(windows, mode) {
        super._init(true);

        this._label = new St.Label({
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this._label);

        this.windows = windows;
        this.icons = [];

        for (let i = 0; i < windows.length; i++) {
            let win = windows[i];
            let icon = new WindowIcon(win, mode);

            this.addItem(icon, icon.label);
            this.icons.push(icon);

            icon.window.connectObject('unmanaged',
                window => this._removeWindow(window), this);
        }

        this.connect('destroy', this._onDestroy.bind(this));
    }

    _onDestroy() {
        this.icons.forEach(
            icon => icon.window.disconnectObject(this));
    }

    vfunc_get_preferred_height(forWidth) {
        let [minHeight, natHeight] = super.vfunc_get_preferred_height(forWidth);

        let spacing = this.get_theme_node().get_padding(St.Side.BOTTOM);
        let [labelMin, labelNat] = this._label.get_preferred_height(-1);

        minHeight += labelMin + spacing;
        natHeight += labelNat + spacing;

        return [minHeight, natHeight];
    }

    vfunc_allocate(box) {
        let themeNode = this.get_theme_node();
        let contentBox = themeNode.get_content_box(box);
        const labelHeight = this._label.height;
        const totalLabelHeight =
            labelHeight + themeNode.get_padding(St.Side.BOTTOM);

        box.y2 -= totalLabelHeight;
        super.vfunc_allocate(box);

        // Hooking up the parent vfunc will call this.set_allocation() with
        // the height without the label height, so call it again with the
        // correct size here.
        box.y2 += totalLabelHeight;
        this.set_allocation(box);

        const childBox = new Clutter.ActorBox();
        childBox.x1 = contentBox.x1;
        childBox.x2 = contentBox.x2;
        childBox.y2 = contentBox.y2;
        childBox.y1 = childBox.y2 - labelHeight;
        this._label.allocate(childBox);
    }

    highlight(index, justOutline) {
        super.highlight(index, justOutline);

        this._label.set_text(index === -1 ? '' : this.icons[index].label.text);
    }

    _removeWindow(window) {
        let index = this.icons.findIndex(icon => {
            return icon.window === window;
        });
        if (index === -1)
            return;

        this.icons.splice(index, 1);
        this.removeItem(index);
    }
});
(uuay)systemActions.js�Kimport AccountsService from 'gi://AccountsService';
import Clutter from 'gi://Clutter';
import Gdm from 'gi://Gdm';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';

import * as GnomeSession from './gnomeSession.js';
import * as LoginManager from './loginManager.js';
import * as Main from '../ui/main.js';
import * as Screenshot from '../ui/screenshot.js';

const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown';
const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';
const DISABLE_USER_SWITCH_KEY = 'disable-user-switching';
const DISABLE_LOCK_SCREEN_KEY = 'disable-lock-screen';
const DISABLE_LOG_OUT_KEY = 'disable-log-out';
const DISABLE_RESTART_KEY = 'disable-restart-buttons';
const ALWAYS_SHOW_LOG_OUT_KEY = 'always-show-log-out';

const POWER_OFF_ACTION_ID        = 'power-off';
const RESTART_ACTION_ID          = 'restart';
const LOCK_SCREEN_ACTION_ID      = 'lock-screen';
const LOGOUT_ACTION_ID           = 'logout';
const SUSPEND_ACTION_ID          = 'suspend';
const SWITCH_USER_ACTION_ID      = 'switch-user';
const LOCK_ORIENTATION_ACTION_ID = 'lock-orientation';
const SCREENSHOT_UI_ACTION_ID    = 'open-screenshot-ui';

let _singleton = null;

/**
 * @returns {SystemActions}
 */
export function getDefault() {
    if (_singleton == null)
        _singleton = new SystemActions();

    return _singleton;
}

const SystemActions = GObject.registerClass({
    Properties: {
        'can-power-off': GObject.ParamSpec.boolean(
            'can-power-off', 'can-power-off', 'can-power-off',
            GObject.ParamFlags.READABLE,
            false),
        'can-restart': GObject.ParamSpec.boolean(
            'can-restart', 'can-restart', 'can-restart',
            GObject.ParamFlags.READABLE,
            false),
        'can-suspend': GObject.ParamSpec.boolean(
            'can-suspend', 'can-suspend', 'can-suspend',
            GObject.ParamFlags.READABLE,
            false),
        'can-lock-screen': GObject.ParamSpec.boolean(
            'can-lock-screen', 'can-lock-screen', 'can-lock-screen',
            GObject.ParamFlags.READABLE,
            false),
        'can-switch-user': GObject.ParamSpec.boolean(
            'can-switch-user', 'can-switch-user', 'can-switch-user',
            GObject.ParamFlags.READABLE,
            false),
        'can-logout': GObject.ParamSpec.boolean(
            'can-logout', 'can-logout', 'can-logout',
            GObject.ParamFlags.READABLE,
            false),
        'can-lock-orientation': GObject.ParamSpec.boolean(
            'can-lock-orientation', 'can-lock-orientation', 'can-lock-orientation',
            GObject.ParamFlags.READABLE,
            false),
        'orientation-lock-icon': GObject.ParamSpec.string(
            'orientation-lock-icon', 'orientation-lock-icon', 'orientation-lock-icon',
            GObject.ParamFlags.READWRITE,
            null),
    },
}, class SystemActions extends GObject.Object {
    _init() {
        super._init();

        this._canHavePowerOff = true;
        this._canHaveSuspend = true;

        function tokenizeKeywords(keywords) {
            return keywords.split(';').map(keyword => GLib.str_tokenize_and_fold(keyword, null)).flat(2);
        }

        this._actions = new Map();
        this._actions.set(POWER_OFF_ACTION_ID, {
            // Translators: The name of the power-off action in search
            name: C_('search-result', 'Power Off'),
            iconName: 'system-shutdown-symbolic',
            // Translators: A list of keywords that match the power-off action, separated by semicolons
            keywords: tokenizeKeywords(_('power off;poweroff;shutdown;halt;stop')),
            available: false,
        });
        this._actions.set(RESTART_ACTION_ID, {
            // Translators: The name of the restart action in search
            name: C_('search-result', 'Restart'),
            iconName: 'system-reboot-symbolic',
            // Translators: A list of keywords that match the restart action, separated by semicolons
            keywords: tokenizeKeywords(_('reboot;restart;')),
            available: false,
        });
        this._actions.set(LOCK_SCREEN_ACTION_ID, {
            // Translators: The name of the lock screen action in search
            name: C_('search-result', 'Lock Screen'),
            iconName: 'system-lock-screen-symbolic',
            // Translators: A list of keywords that match the lock screen action, separated by semicolons
            keywords: tokenizeKeywords(_('lock screen')),
            available: false,
        });
        this._actions.set(LOGOUT_ACTION_ID, {
            // Translators: The name of the logout action in search
            name: C_('search-result', 'Log Out'),
            iconName: 'system-log-out-symbolic',
            // Translators: A list of keywords that match the logout action, separated by semicolons
            keywords: tokenizeKeywords(_('logout;log out;sign off')),
            available: false,
        });
        this._actions.set(SUSPEND_ACTION_ID, {
            // Translators: The name of the suspend action in search
            name: C_('search-result', 'Suspend'),
            iconName: 'media-playback-pause-symbolic',
            // Translators: A list of keywords that match the suspend action, separated by semicolons
            keywords: tokenizeKeywords(_('suspend;sleep')),
            available: false,
        });
        this._actions.set(SWITCH_USER_ACTION_ID, {
            // Translators: The name of the switch user action in search
            name: C_('search-result', 'Switch User'),
            iconName: 'system-switch-user-symbolic',
            // Translators: A list of keywords that match the switch user action, separated by semicolons
            keywords: tokenizeKeywords(_('switch user')),
            available: false,
        });
        this._actions.set(LOCK_ORIENTATION_ACTION_ID, {
            name: '',
            iconName: '',
            // Translators: A list of keywords that match the lock orientation action, separated by semicolons
            keywords: tokenizeKeywords(_('lock orientation;unlock orientation;screen;rotation')),
            available: false,
        });
        this._actions.set(SCREENSHOT_UI_ACTION_ID, {
            // Translators: The name of the screenshot UI action in search
            name: C_('search-result', 'Take a Screenshot'),
            iconName: 'record-screen-symbolic',
            // Translators: A list of keywords that match the screenshot UI action, separated by semicolons
            keywords: tokenizeKeywords(_('screenshot;screencast;snip;capture;record')),
            available: true,
        });

        this._loginScreenSettings = new Gio.Settings({schema_id: LOGIN_SCREEN_SCHEMA});
        this._lockdownSettings = new Gio.Settings({schema_id: LOCKDOWN_SCHEMA});
        this._orientationSettings = new Gio.Settings({schema_id: 'org.gnome.settings-daemon.peripherals.touchscreen'});

        this._session = new GnomeSession.SessionManager();
        this._loginManager = LoginManager.getLoginManager();
        this._monitorManager = global.backend.get_monitor_manager();

        this._userManager = AccountsService.UserManager.get_default();

        this._userManager.connect('notify::is-loaded',
            () => this._updateMultiUser());
        this._userManager.connect('notify::has-multiple-users',
            () => this._updateMultiUser());
        this._userManager.connect('user-added',
            () => this._updateMultiUser());
        this._userManager.connect('user-removed',
            () => this._updateMultiUser());

        this._lockdownSettings.connect(`changed::${DISABLE_USER_SWITCH_KEY}`,
            () => this._updateSwitchUser());
        this._lockdownSettings.connect(`changed::${DISABLE_LOG_OUT_KEY}`,
            () => this._updateLogout());
        global.settings.connect(`changed::${ALWAYS_SHOW_LOG_OUT_KEY}`,
            () => this._updateLogout());

        this._lockdownSettings.connect(`changed::${DISABLE_LOCK_SCREEN_KEY}`,
            () => this._updateLockScreen());

        this._lockdownSettings.connect(`changed::${DISABLE_LOG_OUT_KEY}`,
            () => this._updateHaveShutdown());

        this.forceUpdate();

        this._orientationSettings.connect('changed::orientation-lock', () => {
            this._updateOrientationLock();
            this._updateOrientationLockStatus();
        });
        Main.layoutManager.connect('monitors-changed',
            () => this._updateOrientationLock());
        this._monitorManager.connect('notify::panel-orientation-managed',
            () => this._updateOrientationLock());
        this._updateOrientationLock();
        this._updateOrientationLockStatus();

        Main.sessionMode.connect('updated', () => this._sessionUpdated());
        this._sessionUpdated();
    }

    get canPowerOff() {
        return this._actions.get(POWER_OFF_ACTION_ID).available;
    }

    get canRestart() {
        return this._actions.get(RESTART_ACTION_ID).available;
    }

    get canSuspend() {
        return this._actions.get(SUSPEND_ACTION_ID).available;
    }

    get canLockScreen() {
        return this._actions.get(LOCK_SCREEN_ACTION_ID).available;
    }

    get canSwitchUser() {
        return this._actions.get(SWITCH_USER_ACTION_ID).available;
    }

    get canLogout() {
        return this._actions.get(LOGOUT_ACTION_ID).available;
    }

    get canLockOrientation() {
        return this._actions.get(LOCK_ORIENTATION_ACTION_ID).available;
    }

    get orientationLockIcon() {
        return this._actions.get(LOCK_ORIENTATION_ACTION_ID).iconName;
    }

    _lightdmLoginSession() {
        try {
            let seat = GLib.getenv("XDG_SEAT_PATH");
            let result = Gio.DBus.system.call_sync('org.freedesktop.DisplayManager',
                                                   seat,
                                                   'org.freedesktop.DisplayManager.Seat',
                                                   'SwitchToGreeter', null, null,
                                                   Gio.DBusCallFlags.NONE,
                                                   -1, null);
            return result;
        } catch(e) {
            return false;
        }
    }

    _sensorProxyAppeared() {
        this._sensorProxy = new SensorProxy(Gio.DBus.system, SENSOR_BUS_NAME, SENSOR_OBJECT_PATH,
            (proxy, error)  => {
                if (error) {
                    log(error.message);
                    return;
                }
                this._sensorProxy.connect('g-properties-changed',
                                          () => { this._updateOrientationLock(); });
                this._updateOrientationLock();
            });
    }

    _updateOrientationLock() {
        const available = this._monitorManager.get_panel_orientation_managed();

        this._actions.get(LOCK_ORIENTATION_ACTION_ID).available = available;

        this.notify('can-lock-orientation');
    }

    _updateOrientationLockStatus() {
        let locked = this._orientationSettings.get_boolean('orientation-lock');
        let action = this._actions.get(LOCK_ORIENTATION_ACTION_ID);

        // Translators: The name of the lock orientation action in search
        // and in the system status menu
        let name = locked
            ? C_('search-result', 'Unlock Screen Rotation')
            : C_('search-result', 'Lock Screen Rotation');
        let iconName = locked
            ? 'rotation-locked-symbolic'
            : 'rotation-allowed-symbolic';

        action.name = name;
        action.iconName = iconName;

        this.notify('orientation-lock-icon');
    }

    _sessionUpdated() {
        this._updateLockScreen();
        this._updatePowerOff();
        this._updateSuspend();
        this._updateMultiUser();
    }

    forceUpdate() {
        // Whether those actions are available or not depends on both lockdown
        // settings and Polkit policy - we don't get change notifications for the
        // latter, so their value may be outdated; force an update now
        this._updateHaveShutdown();
        this._updateHaveSuspend();
    }

    getMatchingActions(terms) {
        // terms is a list of strings
        terms = terms.map(
            term => GLib.str_tokenize_and_fold(term, null)[0]).flat(2);

        // tokenizing may return an empty array
        if (terms.length === 0)
            return [];

        let results = [];

        for (let [key, {available, keywords}] of this._actions) {
            if (available && terms.every(t => keywords.some(k => k.startsWith(t))))
                results.push(key);
        }

        return results;
    }

    getName(id) {
        return this._actions.get(id).name;
    }

    getIconName(id) {
        return this._actions.get(id).iconName;
    }

    activateAction(id) {
        switch (id) {
        case POWER_OFF_ACTION_ID:
            this.activatePowerOff();
            break;
        case RESTART_ACTION_ID:
            this.activateRestart();
            break;
        case LOCK_SCREEN_ACTION_ID:
            this.activateLockScreen();
            break;
        case LOGOUT_ACTION_ID:
            this.activateLogout();
            break;
        case SUSPEND_ACTION_ID:
            this.activateSuspend();
            break;
        case SWITCH_USER_ACTION_ID:
            this.activateSwitchUser();
            break;
        case LOCK_ORIENTATION_ACTION_ID:
            this.activateLockOrientation();
            break;
        case SCREENSHOT_UI_ACTION_ID:
            this.activateScreenshotUI();
            break;
        }
    }

    _updateLockScreen() {
        let showLock = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
        let allowLockScreen = !this._lockdownSettings.get_boolean(DISABLE_LOCK_SCREEN_KEY);
        this._actions.get(LOCK_SCREEN_ACTION_ID).available = showLock && allowLockScreen;
        this.notify('can-lock-screen');
    }

    async _updateHaveShutdown() {
        try {
            const [canShutdown] = await this._session.CanShutdownAsync();
            this._canHavePowerOff = canShutdown;
        } catch (e) {
            this._canHavePowerOff = false;
        }
        this._updatePowerOff();
    }

    _updatePowerOff() {
        let disabled = Main.sessionMode.isLocked ||
                       (Main.sessionMode.isGreeter &&
                        this._loginScreenSettings.get_boolean(DISABLE_RESTART_KEY));
        this._actions.get(POWER_OFF_ACTION_ID).available = this._canHavePowerOff && !disabled;
        this.notify('can-power-off');

        this._actions.get(RESTART_ACTION_ID).available = this._canHavePowerOff && !disabled;
        this.notify('can-restart');
    }

    async _updateHaveSuspend() {
        const {canSuspend, needsAuth} = await this._loginManager.canSuspend();
        this._canHaveSuspend = canSuspend;
        this._suspendNeedsAuth = needsAuth;
        this._updateSuspend();
    }

    _updateSuspend() {
        let disabled = (Main.sessionMode.isLocked &&
                        this._suspendNeedsAuth) ||
                       (Main.sessionMode.isGreeter &&
                        this._loginScreenSettings.get_boolean(DISABLE_RESTART_KEY));
        this._actions.get(SUSPEND_ACTION_ID).available = this._canHaveSuspend && !disabled;
        this.notify('can-suspend');
    }

    _updateMultiUser() {
        this._updateLogout();
        this._updateSwitchUser();
    }

    _updateSwitchUser() {
        let allowSwitch = !this._lockdownSettings.get_boolean(DISABLE_USER_SWITCH_KEY);
        let multiUser = this._userManager.can_switch() && this._userManager.has_multiple_users;
        let shouldShowInMode = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;

        let visible = allowSwitch && multiUser && shouldShowInMode;
        this._actions.get(SWITCH_USER_ACTION_ID).available = visible;
        this.notify('can-switch-user');

        return visible;
    }

    _updateLogout() {
        let user = this._userManager.get_user(GLib.get_user_name());

        let allowLogout = !this._lockdownSettings.get_boolean(DISABLE_LOG_OUT_KEY);
        let alwaysShow = global.settings.get_boolean(ALWAYS_SHOW_LOG_OUT_KEY);
        let systemAccount = user.system_account;
        let localAccount = user.local_account;
        let multiUser = this._userManager.has_multiple_users;
        let multiSession = Gdm.get_session_ids().length > 1;
        let shouldShowInMode = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;

        let visible = allowLogout && (alwaysShow || multiUser || multiSession || systemAccount || !localAccount) && shouldShowInMode;
        this._actions.get(LOGOUT_ACTION_ID).available = visible;
        this.notify('can-logout');

        return visible;
    }

    activateLockOrientation() {
        if (!this._actions.get(LOCK_ORIENTATION_ACTION_ID).available)
            throw new Error('The lock-orientation action is not available!');

        let locked = this._orientationSettings.get_boolean('orientation-lock');
        this._orientationSettings.set_boolean('orientation-lock', !locked);
    }

    activateLockScreen() {
        if (!this._actions.get(LOCK_SCREEN_ACTION_ID).available)
            throw new Error('The lock-screen action is not available!');

        if (Main.screenShield)
            Main.screenShield.lock(true);
        else
            this._lightdmLoginSession();
    }

    activateSwitchUser() {
        if (!this._actions.get(SWITCH_USER_ACTION_ID).available)
            throw new Error('The switch-user action is not available!');

        if (Main.screenShield) {
            Main.screenShield.lock(false);

            Clutter.threads_add_repaint_func_full(Clutter.RepaintFlags.POST_PAINT, () => {
                Gdm.goto_login_session_sync(null);
                return false;
            });
        } else
            this._lightdmLoginSession();
    }

    activateLogout() {
        if (!this._actions.get(LOGOUT_ACTION_ID).available)
            throw new Error('The logout action is not available!');

        Main.overview.hide();
        this._session.LogoutAsync(0).catch(logError);
    }

    activatePowerOff() {
        if (!this._actions.get(POWER_OFF_ACTION_ID).available)
            throw new Error('The power-off action is not available!');

        this._session.ShutdownAsync(0).catch(logError);
    }

    activateRestart() {
        if (!this._actions.get(RESTART_ACTION_ID).available)
            throw new Error('The restart action is not available!');

        this._session.RebootAsync().catch(logError);
    }

    activateSuspend() {
        if (!this._actions.get(SUSPEND_ACTION_ID).available)
            throw new Error('The suspend action is not available!');

        this._loginManager.suspend();
    }

    activateScreenshotUI() {
        if (!this._actions.get(SCREENSHOT_UI_ACTION_ID).available)
            throw new Error('The screenshot UI action is not available!');

        if (this._overviewHiddenId)
            return;

        this._overviewHiddenId = Main.overview.connect('hidden', () => {
            Main.overview.disconnect(this._overviewHiddenId);
            delete this._overviewHiddenId;
            Screenshot.showScreenshotUI();
        });
    }
});
(uuay)slider.js�/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';

import * as BarLevel from './barLevel.js';

const SLIDER_SCROLL_STEP = 0.02; /* Slider scrolling step in % */

export const Slider = GObject.registerClass({
    Signals: {
        'drag-begin': {},
        'drag-end': {},
    },
}, class Slider extends BarLevel.BarLevel {
    _init(value) {
        super._init({
            value,
            style_class: 'slider',
            can_focus: true,
            reactive: true,
            accessible_role: Atk.Role.SLIDER,
            x_expand: true,
        });

        this._releaseId = 0;
        this._dragging = false;

        this._handleRadius = 0;
        this._handleBorderWidth = 0;
        this._handleBorderColor = null;

        this._customAccessible.connect('get-minimum-increment', this._getMinimumIncrement.bind(this));
    }

    vfunc_style_changed() {
        super.vfunc_style_changed();

        const themeNode = this.get_theme_node();
        this._handleRadius = themeNode.get_length('-slider-handle-radius');
        this._handleBorderWidth =
            themeNode.get_length('-slider-handle-border-width');
        const [hasHandleColor, handleBorderColor] =
            themeNode.lookup_color('-slider-handle-border-color', false);
        this._handleBorderColor = hasHandleColor ? handleBorderColor : null;
    }

    vfunc_repaint() {
        super.vfunc_repaint();

        // Add handle
        let cr = this.get_context();
        let themeNode = this.get_theme_node();
        let [width, height] = this.get_surface_size();
        const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;

        const ceiledHandleRadius = Math.ceil(this._handleRadius + this._handleBorderWidth);
        const handleY = height / 2;

        let handleX = ceiledHandleRadius +
            (width - 2 * ceiledHandleRadius) * this._value / this._maxValue;
        if (rtl)
            handleX = width - handleX;

        let color = themeNode.get_foreground_color();
        cr.setSourceColor(color);
        cr.arc(handleX, handleY, this._handleRadius, 0, 2 * Math.PI);
        cr.fillPreserve();
        if (this._handleBorderColor && this._handleBorderWidth) {
            cr.setSourceColor(this._handleBorderColor);
            cr.setLineWidth(this._handleBorderWidth);
            cr.stroke();
        }
        cr.$dispose();
    }

    _getPreferredHeight() {
        const barHeight = super._getPreferredHeight();
        const handleHeight = 2 * this._handleRadius + this._handleBorderWidth;
        return Math.max(barHeight, handleHeight);
    }

    _getPreferredWidth() {
        const barWidth = super._getPreferredWidth();
        const handleWidth = 2 * this._handleRadius + this._handleBorderWidth;
        return Math.max(barWidth, handleWidth);
    }

    vfunc_button_press_event(event) {
        return this.startDragging(event);
    }

    startDragging(event) {
        if (this._dragging)
            return Clutter.EVENT_PROPAGATE;

        this._dragging = true;

        let device = event.get_device();
        let sequence = event.get_event_sequence();

        this._grab = global.stage.grab(this);

        this._grabbedDevice = device;
        this._grabbedSequence = sequence;

        // We need to emit 'drag-begin' before moving the handle to make
        // sure that no 'notify::value' signal is emitted before this one.
        this.emit('drag-begin');

        let absX, absY;
        [absX, absY] = event.get_coords();
        this._moveHandle(absX, absY);
        return Clutter.EVENT_STOP;
    }

    _endDragging() {
        if (this._dragging) {
            if (this._releaseId) {
                this.disconnect(this._releaseId);
                this._releaseId = 0;
            }

            if (this._grab) {
                this._grab.dismiss();
                this._grab = null;
            }

            this._grabbedSequence = null;
            this._grabbedDevice = null;
            this._dragging = false;

            this.emit('drag-end');
        }
        return Clutter.EVENT_STOP;
    }

    vfunc_button_release_event() {
        if (this._dragging && !this._grabbedSequence)
            return this._endDragging();

        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_touch_event(event) {
        let sequence = event.get_event_sequence();

        if (!this._dragging &&
            event.type() === Clutter.EventType.TOUCH_BEGIN) {
            this.startDragging(event);
            return Clutter.EVENT_STOP;
        } else if (this._grabbedSequence &&
                   sequence.get_slot() === this._grabbedSequence.get_slot()) {
            if (event.type() === Clutter.EventType.TOUCH_UPDATE)
                return this._motionEvent(this, event);
            else if (event.type() === Clutter.EventType.TOUCH_END)
                return this._endDragging();
        }

        return Clutter.EVENT_PROPAGATE;
    }

    scroll(event) {
        let direction = event.get_scroll_direction();
        let delta = 0;

        if (event.get_flags() & Clutter.EventFlags.FLAG_POINTER_EMULATED)
            return Clutter.EVENT_PROPAGATE;

        if (direction === Clutter.ScrollDirection.DOWN) {
            delta = -SLIDER_SCROLL_STEP;
        } else if (direction === Clutter.ScrollDirection.UP) {
            delta = SLIDER_SCROLL_STEP;
        } else if (direction === Clutter.ScrollDirection.SMOOTH) {
            let [, dy] = event.get_scroll_delta();
            // Even though the slider is horizontal, use dy to match
            // the UP/DOWN above.
            delta = -dy * SLIDER_SCROLL_STEP;
        }

        this.value = Math.min(Math.max(0, this._value + delta), this._maxValue);

        return Clutter.EVENT_STOP;
    }

    vfunc_scroll_event(event) {
        return this.scroll(event);
    }

    vfunc_motion_event(event) {
        if (this._dragging && !this._grabbedSequence)
            return this._motionEvent(this, event);

        return Clutter.EVENT_PROPAGATE;
    }

    _motionEvent(actor, event) {
        let absX, absY;
        [absX, absY] = event.get_coords();
        this._moveHandle(absX, absY);
        return Clutter.EVENT_STOP;
    }

    vfunc_key_press_event(event) {
        let key = event.get_key_symbol();
        if (key === Clutter.KEY_Right || key === Clutter.KEY_Left) {
            let delta = key === Clutter.KEY_Right ? 0.1 : -0.1;
            this.value = Math.max(0, Math.min(this._value + delta, this._maxValue));
            return Clutter.EVENT_STOP;
        }
        return super.vfunc_key_press_event(event);
    }

    _moveHandle(absX, _absY) {
        let relX, sliderX;
        [sliderX] = this.get_transformed_position();
        const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
        let width = this._barLevelWidth;

        relX = absX - sliderX;
        if (rtl)
            relX = width - relX;

        let newvalue;
        if (relX < this._handleRadius)
            newvalue = 0;
        else if (relX > width - this._handleRadius)
            newvalue = 1;
        else
            newvalue = (relX - this._handleRadius) / (width - 2 * this._handleRadius);
        this.value = newvalue * this._maxValue;
    }

    _getMinimumIncrement() {
        return 0.1;
    }
});
(uuay)ibusManager.jsF.// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import IBus from 'gi://IBus';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';

import * as Signals from './signals.js';
import * as BoxPointer from '../ui/boxpointer.js';

import * as IBusCandidatePopup from '../ui/ibusCandidatePopup.js';

Gio._promisify(IBus.Bus.prototype,
    'list_engines_async', 'list_engines_async_finish');
Gio._promisify(IBus.Bus.prototype,
    'request_name_async', 'request_name_async_finish');
Gio._promisify(IBus.Bus.prototype,
    'get_global_engine_async', 'get_global_engine_async_finish');
Gio._promisify(IBus.Bus.prototype,
    'set_global_engine_async', 'set_global_engine_async_finish');
Gio._promisify(Shell, 'util_systemd_unit_exists');

// Ensure runtime version matches
_checkIBusVersion(1, 5, 2);

let _ibusManager = null;
const IBUS_SYSTEMD_SERVICE = 'org.freedesktop.IBus.session.GNOME.service';

const TYPING_BOOSTER_ENGINE = 'typing-booster';

function _checkIBusVersion(requiredMajor, requiredMinor, requiredMicro) {
    if ((IBus.MAJOR_VERSION > requiredMajor) ||
        (IBus.MAJOR_VERSION === requiredMajor && IBus.MINOR_VERSION > requiredMinor) ||
        (IBus.MAJOR_VERSION === requiredMajor && IBus.MINOR_VERSION === requiredMinor &&
         IBus.MICRO_VERSION >= requiredMicro))
        return;

    throw new Error(`Found IBus version ${
        IBus.MAJOR_VERSION}.${IBus.MINOR_VERSION}.${IBus.MINOR_VERSION} ` +
        `but required is ${requiredMajor}.${requiredMinor}.${requiredMicro}`);
}

/**
 * @returns {IBusManager}
 */
export function getIBusManager() {
    if (_ibusManager == null)
        _ibusManager = new IBusManager();
    return _ibusManager;
}

class IBusManager extends Signals.EventEmitter {
    constructor() {
        super();

        IBus.init();

        // This is the longest we'll keep the keyboard frozen until an input
        // source is active.
        this._MAX_INPUT_SOURCE_ACTIVATION_TIME = 4000; // ms
        this._PRELOAD_ENGINES_DELAY_TIME = 30; // sec


        this._candidatePopup = new IBusCandidatePopup.CandidatePopup();

        this._panelService = null;
        this._engines = new Map();
        this._ready = false;
        this._registerPropertiesId = 0;
        this._currentEngineName = null;
        this._preloadEnginesId = 0;

        this._ibus = IBus.Bus.new_async();
        this._ibus.connect('connected', this._onConnected.bind(this));
        this._ibus.connect('disconnected', this._clear.bind(this));
        // Need to set this to get 'global-engine-changed' emitions
        this._ibus.set_watch_ibus_signal(true);
        this._ibus.connect('global-engine-changed', this._engineChanged.bind(this));

        this._queueSpawn();
    }

    async _ibusSystemdServiceExists() {
        if (this._ibusIsSystemdService)
            return true;

        try {
            this._ibusIsSystemdService =
                await Shell.util_systemd_unit_exists(
                    IBUS_SYSTEMD_SERVICE, null);
        } catch (e) {
            this._ibusIsSystemdService = false;
        }

        return this._ibusIsSystemdService;
    }

    async _queueSpawn() {
        const isSystemdService = await this._ibusSystemdServiceExists();
        if (!isSystemdService)
            this._spawn(Meta.is_wayland_compositor() ? [] : ['--xim']);
    }

    _tryAppendEnv(env, varname) {
        const value = GLib.getenv(varname);
        if (value)
            env.push(`${varname}=${value}`);
    }

    _spawn(extraArgs = []) {
        try {
            const cmdLine = ['ibus-daemon', '--panel', 'disable', ...extraArgs];
            const launchContext = global.create_app_launch_context(0, -1);
            const env = launchContext.get_environment();
            // Use DO_NOT_REAP_CHILD to avoid adouble-fork internally
            // since ibus-daemon refuses to start with init as its parent.
            const pid = Shell.util_spawn_async(
                null, cmdLine, env,
                GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD);
            GLib.child_watch_add(
                GLib.PRIORITY_DEFAULT,
                pid,
                () => GLib.spawn_close_pid(pid)
            );
        } catch (e) {
            log(`Failed to launch ibus-daemon: ${e.message}`);
        }
    }

    async restartDaemon(extraArgs = []) {
        const isSystemdService = await this._ibusSystemdServiceExists();
        if (!isSystemdService)
            this._spawn(['-r', ...extraArgs]);
    }

    _clear() {
        if (this._cancellable) {
            this._cancellable.cancel();
            this._cancellable = null;
        }

        if (this._preloadEnginesId) {
            GLib.source_remove(this._preloadEnginesId);
            this._preloadEnginesId = 0;
        }

        if (this._panelService)
            this._panelService.destroy();

        this._panelService = null;
        this._candidatePopup.setPanelService(null);
        this._engines.clear();
        this._ready = false;
        this._registerPropertiesId = 0;
        this._currentEngineName = null;

        this.emit('ready', false);
    }

    _onConnected() {
        this._cancellable = new Gio.Cancellable();
        this._initEngines();
        this._initPanelService();
    }

    async _initEngines() {
        try {
            const enginesList =
                await this._ibus.list_engines_async(-1, this._cancellable);
            for (let i = 0; i < enginesList.length; ++i) {
                let name = enginesList[i].get_name();
                this._engines.set(name, enginesList[i]);
            }
            this._updateReadiness();
        } catch (e) {
            if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                return;

            logError(e);
            this._clear();
        }
    }

    async _initPanelService() {
        try {
            await this._ibus.request_name_async(IBus.SERVICE_PANEL,
                IBus.BusNameFlag.REPLACE_EXISTING, -1, this._cancellable);
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
                logError(e);
                this._clear();
            }
            return;
        }

        this._panelService = new IBus.PanelService({
            connection: this._ibus.get_connection(),
            object_path: IBus.PATH_PANEL,
        });
        this._candidatePopup.setPanelService(this._panelService);
        this._panelService.connect('update-property', this._updateProperty.bind(this));
        this._panelService.connect('set-cursor-location', (ps, x, y, w, h) => {
            let cursorLocation = {x, y, width: w, height: h};
            this.emit('set-cursor-location', cursorLocation);
        });
        this._panelService.connect('focus-in', (panel, path) => {
            if (!GLib.str_has_suffix(path, '/InputContext_1'))
                this.emit('focus-in');
        });
        this._panelService.connect('focus-out', () => this.emit('focus-out'));

        try {
            // IBus versions older than 1.5.10 have a bug which
            // causes spurious set-content-type emissions when
            // switching input focus that temporarily lose purpose
            // and hints defeating its intended semantics and
            // confusing users. We thus don't use it in that case.
            _checkIBusVersion(1, 5, 10);
            this._panelService.connect('set-content-type', this._setContentType.bind(this));
        } catch (e) {
        }
        this._updateReadiness();

        try {
            // If an engine is already active we need to get its properties
            const engine =
                await this._ibus.get_global_engine_async(-1, this._cancellable);
            this._engineChanged(this._ibus, engine.get_name());
        } catch (e) {
        }
    }

    _updateReadiness() {
        this._ready = this._engines.size > 0 && this._panelService != null;
        this.emit('ready', this._ready);
    }

    _engineChanged(bus, engineName) {
        if (!this._ready)
            return;

        this._currentEngineName = engineName;
        this._candidatePopup.close(BoxPointer.PopupAnimation.NONE);

        if (this._registerPropertiesId !== 0)
            return;

        this._registerPropertiesId =
            this._panelService.connect('register-properties', (p, props) => {
                if (!props.get(0))
                    return;

                this._panelService.disconnect(this._registerPropertiesId);
                this._registerPropertiesId = 0;

                this.emit('properties-registered', this._currentEngineName, props);
            });
    }

    _updateProperty(panel, prop) {
        this.emit('property-updated', this._currentEngineName, prop);
    }

    _setContentType(panel, purpose, hints) {
        this.emit('set-content-type', purpose, hints);
    }

    activateProperty(key, state) {
        this._panelService.property_activate(key, state);
    }

    getEngineDesc(id) {
        if (!this._ready || !this._engines.has(id))
            return null;

        return this._engines.get(id);
    }

    async _setEngine(id, callback) {
        // Send id even if id == this._currentEngineName
        // because 'properties-registered' signal can be emitted
        // while this._ibusSources == null on a lock screen.
        if (!this._ready) {
            if (callback)
                callback();
            return;
        }

        try {
            await this._ibus.set_global_engine_async(id,
                this._MAX_INPUT_SOURCE_ACTIVATION_TIME,
                this._cancellable);
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                logError(e);
        }

        if (callback)
            callback();
    }

    async setEngine(id, callback) {
        if (this._oskCompletion)
            this._preOskEngine = id;

        const isXkb = id.startsWith('xkb:');
        if (this._oskCompletion && isXkb)
            return;

        if (this._oskCompletion)
            this.setCompletionEnabled(false, callback);
        else
            await this._setEngine(id, callback);
    }

    preloadEngines(ids) {
        if (!this._ibus || !this._ready)
            return;

        if (!ids.includes(TYPING_BOOSTER_ENGINE))
            ids.push(TYPING_BOOSTER_ENGINE);

        if (this._preloadEnginesId !== 0) {
            GLib.source_remove(this._preloadEnginesId);
            this._preloadEnginesId = 0;
        }

        this._preloadEnginesId =
            GLib.timeout_add_seconds(
                GLib.PRIORITY_DEFAULT,
                this._PRELOAD_ENGINES_DELAY_TIME,
                () => {
                    this._ibus.preload_engines_async(
                        ids,
                        -1,
                        this._cancellable,
                        null);
                    this._preloadEnginesId = 0;
                    return GLib.SOURCE_REMOVE;
                });
    }

    setCompletionEnabled(enabled, callback) {
        /* Needs typing-booster available */
        if (enabled && !this._engines.has(TYPING_BOOSTER_ENGINE))
            return false;
        /* Can do only on xkb engines */
        if (enabled && !this._currentEngineName.startsWith('xkb:'))
            return false;

        if (this._oskCompletion === enabled)
            return true;

        this._oskCompletion = enabled;

        if (enabled) {
            this._preOskEngine = this._currentEngineName;
            this._setEngine(TYPING_BOOSTER_ENGINE, callback);
        } else if (this._preOskEngine) {
            this._setEngine(this._preOskEngine, callback);
            delete this._preOskEngine;
        }
        return true;
    }
}
(uuay)history.js;// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import * as Signals from './signals.js';
import Clutter from 'gi://Clutter';
import * as Params from './params.js';

const DEFAULT_LIMIT = 512;

export class HistoryManager extends Signals.EventEmitter {
    constructor(params) {
        super();

        params = Params.parse(params, {
            gsettingsKey: null,
            limit: DEFAULT_LIMIT,
            entry: null,
        });

        this._key = params.gsettingsKey;
        this._limit = params.limit;

        this._historyIndex = 0;
        if (this._key) {
            this._history = global.settings.get_strv(this._key);
            global.settings.connect(`changed::${this._key}`,
                this._historyChanged.bind(this));
        } else {
            this._history = [];
        }

        this._entry = params.entry;

        if (this._entry) {
            this._entry.connect('key-press-event',
                this._onEntryKeyPress.bind(this));
        }
    }

    _historyChanged() {
        this._history = global.settings.get_strv(this._key);
        this._historyIndex = this._history.length;
    }

    _setPrevItem(text) {
        if (this._historyIndex <= 0)
            return false;

        if (text)
            this._history[this._historyIndex] = text;
        this._historyIndex--;
        this._indexChanged();
        return true;
    }

    _setNextItem(text) {
        if (this._historyIndex >= this._history.length)
            return false;

        if (text)
            this._history[this._historyIndex] = text;
        this._historyIndex++;
        this._indexChanged();
        return true;
    }

    lastItem() {
        if (this._historyIndex !== this._history.length) {
            this._historyIndex = this._history.length;
            this._indexChanged();
        }

        return this._historyIndex ? this._history[this._historyIndex - 1] : null;
    }

    addItem(input) {
        input = input.trim();
        if (input &&
            (this._history.length === 0 ||
             this._history[this._history.length - 1] !== input)) {
            this._history = this._history.filter(entry => entry !== input);
            this._history.push(input);
            this._save();
        }
        this._historyIndex = this._history.length;
        return input; // trimmed
    }

    _onEntryKeyPress(entry, event) {
        let symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_Up)
            return this._setPrevItem(entry.get_text().trim());
        else if (symbol === Clutter.KEY_Down)
            return this._setNextItem(entry.get_text().trim());

        return Clutter.EVENT_PROPAGATE;
    }

    _indexChanged() {
        let current = this._history[this._historyIndex] || '';
        this.emit('changed', current);

        if (this._entry)
            this._entry.set_text(current);
    }

    _save() {
        if (this._history.length > this._limit)
            this._history.splice(0, this._history.length - this._limit);

        if (this._key)
            global.settings.set_strv(this._key, this._history);
    }
}
(uuay)magnifier.js�$// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atspi from 'gi://Atspi';
import Clutter from 'gi://Clutter';
import GDesktopEnums from 'gi://GDesktopEnums';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';

import * as Background from './background.js';
import * as FocusCaretTracker from './focusCaretTracker.js';
import * as Main from './main.js';
import * as Params from '../misc/params.js';
import * as PointerWatcher from './pointerWatcher.js';

const CROSSHAIRS_CLIP_SIZE = [100, 100];
const NO_CHANGE = 0.0;

const POINTER_REST_TIME = 1000; // milliseconds

// Settings
const MAGNIFIER_SCHEMA          = 'org.gnome.desktop.a11y.magnifier';
const SCREEN_POSITION_KEY       = 'screen-position';
const MAG_FACTOR_KEY            = 'mag-factor';
const INVERT_LIGHTNESS_KEY      = 'invert-lightness';
const COLOR_SATURATION_KEY      = 'color-saturation';
const BRIGHT_RED_KEY            = 'brightness-red';
const BRIGHT_GREEN_KEY          = 'brightness-green';
const BRIGHT_BLUE_KEY           = 'brightness-blue';
const CONTRAST_RED_KEY          = 'contrast-red';
const CONTRAST_GREEN_KEY        = 'contrast-green';
const CONTRAST_BLUE_KEY         = 'contrast-blue';
const LENS_MODE_KEY             = 'lens-mode';
const CLAMP_MODE_KEY            = 'scroll-at-edges';
const MOUSE_TRACKING_KEY        = 'mouse-tracking';
const FOCUS_TRACKING_KEY        = 'focus-tracking';
const CARET_TRACKING_KEY        = 'caret-tracking';
const SHOW_CROSS_HAIRS_KEY      = 'show-cross-hairs';
const CROSS_HAIRS_THICKNESS_KEY = 'cross-hairs-thickness';
const CROSS_HAIRS_COLOR_KEY     = 'cross-hairs-color';
const CROSS_HAIRS_OPACITY_KEY   = 'cross-hairs-opacity';
const CROSS_HAIRS_LENGTH_KEY    = 'cross-hairs-length';
const CROSS_HAIRS_CLIP_KEY      = 'cross-hairs-clip';

const MouseSpriteContent = GObject.registerClass({
    Implements: [Clutter.Content],
}, class MouseSpriteContent extends GObject.Object {
    _init() {
        super._init();
        this._scale = 1.0;
        this._monitorScale = 1.0;
        this._texture = null;
    }

    vfunc_get_preferred_size() {
        if (!this._texture)
            return [false, 0, 0];

        let width = this._texture.get_width() / this._scale;
        let height = this._texture.get_height() / this._scale;

        return [true, width, height];
    }

    vfunc_paint_content(actor, node, _paintContext) {
        if (!this._texture)
            return;

        let [minFilter, magFilter] = actor.get_content_scaling_filters();
        let textureNode = new Clutter.TextureNode(this._texture,
            null, minFilter, magFilter);
        textureNode.set_name('MouseSpriteContent');
        node.add_child(textureNode);

        textureNode.add_rectangle(actor.get_content_box());
    }

    _textureScale() {
        if (!this._texture)
            return 1;

        /* This is a workaround to guess the sprite scale; while it works file
         * in normal scenarios, it's not guaranteed to work in all the cases,
         * and so we should actually add an API to mutter that will allow us
         * to know the real spirte texture scaling in order to adapt it to the
         * wanted one. */
        let avgSize = (this._texture.get_width() + this._texture.get_height()) / 2;
        return Math.max(1, Math.floor(avgSize / Meta.prefs_get_cursor_size() + .1));
    }

    _recomputeScale() {
        let scale = this._textureScale() / this._monitorScale;

        if (this._scale !== scale) {
            this._scale = scale;
            return true;
        }
        return false;
    }

    get texture() {
        return this._texture;
    }

    set texture(coglTexture) {
        if (this._texture === coglTexture)
            return;

        let oldTexture = this._texture;
        this._texture = coglTexture;
        this.invalidate();

        if (!oldTexture || !coglTexture ||
            oldTexture.get_width() !== coglTexture.get_width() ||
            oldTexture.get_height() !== coglTexture.get_height()) {
            this._recomputeScale();
            this.invalidate_size();
        }
    }

    get scale() {
        return this._scale;
    }

    set monitorScale(monitorScale) {
        this._monitorScale = monitorScale;
        if (this._recomputeScale())
            this.invalidate_size();
    }
});

export class Magnifier extends Signals.EventEmitter {
    constructor() {
        super();

        // Magnifier is a manager of ZoomRegions.
        this._zoomRegions = [];

        // Create small clutter tree for the magnified mouse.
        let cursorTracker = Meta.CursorTracker.get_for_display(global.display);
        this._cursorTracker = cursorTracker;

        this._mouseSprite = new Clutter.Actor({request_mode: Clutter.RequestMode.CONTENT_SIZE});
        this._mouseSprite.content = new MouseSpriteContent();

        this._cursorRoot = new Clutter.Actor();
        this._cursorRoot.add_child(this._mouseSprite);

        // Create the first ZoomRegion and initialize it according to the
        // magnification settings.

        [this.xMouse, this.yMouse] = global.get_pointer();

        let aZoomRegion = new ZoomRegion(this, this._cursorRoot);
        this._zoomRegions.push(aZoomRegion);
        this._settingsInit(aZoomRegion);
        aZoomRegion.scrollContentsTo(this.xMouse, this.yMouse);

        this._updateContentScale();

        St.Settings.get().connect('notify::magnifier-active', () => {
            this.setActive(St.Settings.get().magnifier_active);
        });

        this.setActive(St.Settings.get().magnifier_active);
        this._cursorUnfocusInhibited = false;
    }

    _updateContentScale() {
        let monitor = Main.layoutManager.findMonitorForPoint(this.xMouse,
            this.yMouse);
        this._mouseSprite.content.monitorScale = monitor
            ? monitor.geometry_scale : 1;
    }

    /**
     * showSystemCursor:
     * Show the system mouse pointer.
     */
    showSystemCursor() {
        const seat = Clutter.get_default_backend().get_default_seat();

        if (this._cursorUnfocusInhibited) {
            seat.uninhibit_unfocus();
            this._cursorUnfocusInhibited = false;
        }

        if (this._cursorVisibilityChangedId) {
            this._cursorTracker.disconnect(this._cursorVisibilityChangedId);
            delete this._cursorVisibilityChangedId;

            this._cursorTracker.set_pointer_visible(true);
        }
    }

    /**
     * hideSystemCursor:
     * Hide the system mouse pointer.
     */
    hideSystemCursor() {
        const seat = Clutter.get_default_backend().get_default_seat();

        if (!this._cursorUnfocusInhibited) {
            seat.inhibit_unfocus();
            this._cursorUnfocusInhibited = true;
        }

        if (!this._cursorVisibilityChangedId) {
            this._cursorTracker.set_pointer_visible(false);
            this._cursorVisibilityChangedId = this._cursorTracker.connect('visibility-changed', () => {
                if (this._cursorTracker.get_pointer_visible())
                    this._cursorTracker.set_pointer_visible(false);
            });
        }
    }

    /**
     * setActive:
     * Show/hide all the zoom regions.
     *
     * @param {boolean} activate Boolean to activate or de-activate the magnifier.
     */
    setActive(activate) {
        let isActive = this.isActive();

        this._zoomRegions.forEach(zoomRegion => {
            zoomRegion.setActive(activate);
        });

        if (isActive === activate)
            return;

        if (activate) {
            this._updateMouseSprite();
            this._cursorTracker.connectObject(
                'cursor-changed', this._updateMouseSprite.bind(this), this);
            Meta.disable_unredirect_for_display(global.display);
            this.startTrackingMouse();
        } else {
            this._cursorTracker.disconnectObject(this);
            this._mouseSprite.content.texture = null;
            Meta.enable_unredirect_for_display(global.display);
            this.stopTrackingMouse();
        }

        if (this._crossHairs)
            this._crossHairs.setEnabled(activate);

        // Make sure system mouse pointer is shown when all zoom regions are
        // invisible.
        if (!activate)
            this.showSystemCursor();

        // Notify interested parties of this change
        this.emit('active-changed', activate);
    }

    /**
     * isActive:
     *
     * @returns {boolean} Whether the magnifier is active.
     */
    isActive() {
        // Sufficient to check one ZoomRegion since Magnifier's active
        // state applies to all of them.
        if (this._zoomRegions.length === 0)
            return false;
        else
            return this._zoomRegions[0].isActive();
    }

    /**
     * startTrackingMouse:
     * Turn on mouse tracking, if not already doing so.
     */
    startTrackingMouse() {
        if (!this._pointerWatch) {
            let interval = 1000 / 60;
            this._pointerWatch = PointerWatcher.getPointerWatcher().addWatch(interval, this.scrollToMousePos.bind(this));

            this.scrollToMousePos();
        }
    }

    /**
     * stopTrackingMouse:
     * Turn off mouse tracking, if not already doing so.
     */
    stopTrackingMouse() {
        if (this._pointerWatch)
            this._pointerWatch.remove();

        this._pointerWatch = null;
    }

    /**
     * isTrackingMouse:
     *
     * @returns {boolean} whether the magnifier is currently tracking the mouse
     */
    isTrackingMouse() {
        return !!this._pointerWatch;
    }

    /**
     * scrollToMousePos:
     * Position all zoom regions' ROI relative to the current location of the
     * system pointer.
     *
     * @param {[xMouse: number, yMouse: number] | []} args
     */
    scrollToMousePos(...args) {
        const [xMouse, yMouse] = args.length ? args : global.get_pointer();

        if (xMouse === this.xMouse && yMouse === this.yMouse)
            return;

        this.xMouse = xMouse;
        this.yMouse = yMouse;

        this._updateContentScale();

        let sysMouseOverAny = false;
        this._zoomRegions.forEach(zoomRegion => {
            if (zoomRegion.scrollToMousePos())
                sysMouseOverAny = true;
        });
        if (sysMouseOverAny)
            this.hideSystemCursor();
        else
            this.showSystemCursor();
    }

    /**
     * createZoomRegion:
     * Create a ZoomRegion instance with the given properties.
     *
     * @param {number} xMagFactor
     *     The power to set horizontal magnification of the ZoomRegion. A value
     *     of 1.0 means no magnification, a value of 2.0 doubles the size.
     * @param {number} yMagFactor
     *    The power to set the vertical magnification of the ZoomRegion.
     * @param {{x: number, y: number, width: number, height: number}} roi
     *    The reg Object that defines the region to magnify, given in
     *    unmagnified coordinates.
     * @param {{x: number, y: number, width: number, height: number}} viewPort
     *     Object that defines the position of the ZoomRegion on screen.
     * @returns {ZoomRegion} the newly created ZoomRegion.
     */
    createZoomRegion(xMagFactor, yMagFactor, roi, viewPort) {
        let zoomRegion = new ZoomRegion(this, this._cursorRoot);
        zoomRegion.setViewPort(viewPort);

        // We ignore the redundant width/height on the ROI
        let fixedROI = Object.create(roi);
        fixedROI.width = viewPort.width / xMagFactor;
        fixedROI.height = viewPort.height / yMagFactor;
        zoomRegion.setROI(fixedROI);

        zoomRegion.addCrosshairs(this._crossHairs);
        return zoomRegion;
    }

    /**
     * addZoomRegion:
     * Append the given ZoomRegion to the list of currently defined ZoomRegions
     * for this Magnifier instance.
     *
     * @param {ZoomRegion} zoomRegion The zoomRegion to add.
     */
    addZoomRegion(zoomRegion) {
        if (zoomRegion) {
            this._zoomRegions.push(zoomRegion);
            if (!this.isTrackingMouse())
                this.startTrackingMouse();
        }
    }

    /**
     * getZoomRegions:
     * Return a list of ZoomRegion's for this Magnifier.
     *
     * @returns {ZoomRegion[]} The Magnifier's zoom region list.
     */
    getZoomRegions() {
        return this._zoomRegions;
    }

    /**
     * clearAllZoomRegions:
     * Remove all the zoom regions from this Magnfier's ZoomRegion list.
     */
    clearAllZoomRegions() {
        for (let i = 0; i < this._zoomRegions.length; i++)
            this._zoomRegions[i].setActive(false);

        this._zoomRegions.length = 0;
        this.stopTrackingMouse();
        this.showSystemCursor();
    }

    /**
     * addCrosshairs:
     * Add and show a cross hair centered on the magnified mouse.
     */
    addCrosshairs() {
        if (!this._crossHairs)
            this._crossHairs = new Crosshairs();

        let thickness = this._settings.get_int(CROSS_HAIRS_THICKNESS_KEY);
        let color = this._settings.get_string(CROSS_HAIRS_COLOR_KEY);
        let opacity = this._settings.get_double(CROSS_HAIRS_OPACITY_KEY);
        let length = this._settings.get_int(CROSS_HAIRS_LENGTH_KEY);
        let clip = this._settings.get_boolean(CROSS_HAIRS_CLIP_KEY);

        this.setCrosshairsThickness(thickness);
        this.setCrosshairsColor(color);
        this.setCrosshairsOpacity(opacity);
        this.setCrosshairsLength(length);
        this.setCrosshairsClip(clip);

        let theCrossHairs = this._crossHairs;
        this._zoomRegions.forEach(zoomRegion => {
            zoomRegion.addCrosshairs(theCrossHairs);
        });
    }

    /**
     * setCrosshairsVisible:
     *
     * Show or hide the cross hair
     *
     * @param {boolean} visible Flag that indicates show (true) or hide (false).
     */
    setCrosshairsVisible(visible) {
        if (visible) {
            if (!this._crossHairs)
                this.addCrosshairs();
            this._crossHairs.show();
        } else {
            // eslint-disable-next-line no-lonely-if
            if (this._crossHairs)
                this._crossHairs.hide();
        }
    }

    /**
     * setCrosshairsColor:
     *
     * Set the color of the crosshairs for all ZoomRegions.
     *
     * @param {string} color The color as a string, e.g. '#ff0000ff' or 'red'.
     */
    setCrosshairsColor(color) {
        if (this._crossHairs) {
            let [res_, clutterColor] = Clutter.Color.from_string(color);
            this._crossHairs.setColor(clutterColor);
        }
    }

    /**
     * setCrosshairsThickness:
     *
     * Set the crosshairs thickness for all ZoomRegions.
     *
     * @param {number} thickness The width of the vertical and
     *     horizontal lines of the crosshairs.
     */
    setCrosshairsThickness(thickness) {
        if (this._crossHairs)
            this._crossHairs.setThickness(thickness);
    }

    /**
     * Get the crosshairs thickness.
     *
     * @returns {number} The width of the vertical and horizontal
     *     lines of the crosshairs.
     */
    getCrosshairsThickness() {
        if (this._crossHairs)
            return this._crossHairs.getThickness();
        else
            return 0;
    }

    /**
     * @param {number} opacity Value between 0.0 (transparent)
     *     and 1.0 (fully opaque).
     */
    setCrosshairsOpacity(opacity) {
        if (this._crossHairs)
            this._crossHairs.setOpacity(opacity * 255);
    }

    /**
     * @returns {number} Value between 0.0 (transparent) and 1.0 (fully opaque).
     */
    getCrosshairsOpacity() {
        if (this._crossHairs)
            return this._crossHairs.getOpacity() / 255.0;
        else
            return 0.0;
    }

    /**
     * Set the crosshairs length for all ZoomRegions.
     *
     * @param {number} length The length of the vertical and horizontal
     *     lines making up the crosshairs.
     */
    setCrosshairsLength(length) {
        if (this._crossHairs) {
            let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
            this._crossHairs.setLength(length / scaleFactor);
        }
    }

    /**
     * getCrosshairsLength:
     * Get the crosshairs length.
     *
     * @returns {number} The length of the vertical and horizontal
     *     lines making up the crosshairs.
     */
    getCrosshairsLength() {
        if (this._crossHairs)
            return this._crossHairs.getLength();
        else
            return 0;
    }

    /**
     * setCrosshairsClip:
     *
     * Set whether the crosshairs are clipped at their intersection.
     *
     * @param {boolean} clip Flag to indicate whether to clip the crosshairs.
     */
    setCrosshairsClip(clip) {
        if (!this._crossHairs)
            return;

        // Setting no clipping on crosshairs means a zero sized clip rectangle.
        this._crossHairs.setClip(clip ? CROSSHAIRS_CLIP_SIZE : [0, 0]);
    }

    /**
     * getCrosshairsClip:
     * Get whether the crosshairs are clipped by the mouse image.
     *
     * @returns {boolean} Whether the crosshairs are clipped.
     */
    getCrosshairsClip() {
        if (this._crossHairs) {
            let [clipWidth, clipHeight] = this._crossHairs.getClip();
            return clipWidth > 0 && clipHeight > 0;
        } else {
            return false;
        }
    }

    // Private methods //

    _updateMouseSprite() {
        this._updateSpriteTexture();
        let [xHot, yHot] = this._cursorTracker.get_hot();
        this._mouseSprite.set({
            translation_x: -xHot,
            translation_y: -yHot,
        });
    }

    _updateSpriteTexture() {
        let sprite = this._cursorTracker.get_sprite();

        if (sprite) {
            this._mouseSprite.content.texture = sprite;
            this._mouseSprite.show();
        } else {
            this._mouseSprite.hide();
        }
    }

    _settingsInit(zoomRegion) {
        this._settings = new Gio.Settings({schema_id: MAGNIFIER_SCHEMA});

        this._settings.connect(`changed::${SCREEN_POSITION_KEY}`,
            this._updateScreenPosition.bind(this));
        this._settings.connect(`changed::${MAG_FACTOR_KEY}`,
            this._updateMagFactor.bind(this));
        this._settings.connect(`changed::${LENS_MODE_KEY}`,
            this._updateLensMode.bind(this));
        this._settings.connect(`changed::${CLAMP_MODE_KEY}`,
            this._updateClampMode.bind(this));
        this._settings.connect(`changed::${MOUSE_TRACKING_KEY}`,
            this._updateMouseTrackingMode.bind(this));
        this._settings.connect(`changed::${FOCUS_TRACKING_KEY}`,
            this._updateFocusTrackingMode.bind(this));
        this._settings.connect(`changed::${CARET_TRACKING_KEY}`,
            this._updateCaretTrackingMode.bind(this));

        this._settings.connect(`changed::${INVERT_LIGHTNESS_KEY}`,
            this._updateInvertLightness.bind(this));
        this._settings.connect(`changed::${COLOR_SATURATION_KEY}`,
            this._updateColorSaturation.bind(this));

        this._settings.connect(`changed::${BRIGHT_RED_KEY}`,
            this._updateBrightness.bind(this));
        this._settings.connect(`changed::${BRIGHT_GREEN_KEY}`,
            this._updateBrightness.bind(this));
        this._settings.connect(`changed::${BRIGHT_BLUE_KEY}`,
            this._updateBrightness.bind(this));

        this._settings.connect(`changed::${CONTRAST_RED_KEY}`,
            this._updateContrast.bind(this));
        this._settings.connect(`changed::${CONTRAST_GREEN_KEY}`,
            this._updateContrast.bind(this));
        this._settings.connect(`changed::${CONTRAST_BLUE_KEY}`,
            this._updateContrast.bind(this));

        this._settings.connect(`changed::${SHOW_CROSS_HAIRS_KEY}`, () => {
            this.setCrosshairsVisible(this._settings.get_boolean(SHOW_CROSS_HAIRS_KEY));
        });

        this._settings.connect(`changed::${CROSS_HAIRS_THICKNESS_KEY}`, () => {
            this.setCrosshairsThickness(this._settings.get_int(CROSS_HAIRS_THICKNESS_KEY));
        });

        this._settings.connect(`changed::${CROSS_HAIRS_COLOR_KEY}`, () => {
            this.setCrosshairsColor(this._settings.get_string(CROSS_HAIRS_COLOR_KEY));
        });

        this._settings.connect(`changed::${CROSS_HAIRS_OPACITY_KEY}`, () => {
            this.setCrosshairsOpacity(this._settings.get_double(CROSS_HAIRS_OPACITY_KEY));
        });

        this._settings.connect(`changed::${CROSS_HAIRS_LENGTH_KEY}`, () => {
            this.setCrosshairsLength(this._settings.get_int(CROSS_HAIRS_LENGTH_KEY));
        });

        this._settings.connect(`changed::${CROSS_HAIRS_CLIP_KEY}`, () => {
            this.setCrosshairsClip(this._settings.get_boolean(CROSS_HAIRS_CLIP_KEY));
        });

        if (zoomRegion) {
            // Mag factor is accurate to two decimal places.
            let aPref = parseFloat(this._settings.get_double(MAG_FACTOR_KEY).toFixed(2));
            if (aPref !== 0.0)
                zoomRegion.setMagFactor(aPref, aPref);

            aPref = this._settings.get_enum(SCREEN_POSITION_KEY);
            if (aPref)
                zoomRegion.setScreenPosition(aPref);

            zoomRegion.setLensMode(this._settings.get_boolean(LENS_MODE_KEY));
            zoomRegion.setClampScrollingAtEdges(!this._settings.get_boolean(CLAMP_MODE_KEY));

            aPref = this._settings.get_enum(MOUSE_TRACKING_KEY);
            if (aPref)
                zoomRegion.setMouseTrackingMode(aPref);

            aPref = this._settings.get_enum(FOCUS_TRACKING_KEY);
            if (aPref)
                zoomRegion.setFocusTrackingMode(aPref);

            aPref = this._settings.get_enum(CARET_TRACKING_KEY);
            if (aPref)
                zoomRegion.setCaretTrackingMode(aPref);

            aPref = this._settings.get_boolean(INVERT_LIGHTNESS_KEY);
            if (aPref)
                zoomRegion.setInvertLightness(aPref);

            aPref = this._settings.get_double(COLOR_SATURATION_KEY);
            if (aPref)
                zoomRegion.setColorSaturation(aPref);

            let bc = {};
            bc.r = this._settings.get_double(BRIGHT_RED_KEY);
            bc.g = this._settings.get_double(BRIGHT_GREEN_KEY);
            bc.b = this._settings.get_double(BRIGHT_BLUE_KEY);
            zoomRegion.setBrightness(bc);

            bc.r = this._settings.get_double(CONTRAST_RED_KEY);
            bc.g = this._settings.get_double(CONTRAST_GREEN_KEY);
            bc.b = this._settings.get_double(CONTRAST_BLUE_KEY);
            zoomRegion.setContrast(bc);
        }

        let showCrosshairs = this._settings.get_boolean(SHOW_CROSS_HAIRS_KEY);
        this.addCrosshairs();
        this.setCrosshairsVisible(showCrosshairs);
    }

    _updateScreenPosition() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            let position = this._settings.get_enum(SCREEN_POSITION_KEY);
            this._zoomRegions[0].setScreenPosition(position);
            if (position !== GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN)
                this._updateLensMode();
        }
    }

    _updateMagFactor() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            // Mag factor is accurate to two decimal places.
            let magFactor = parseFloat(this._settings.get_double(MAG_FACTOR_KEY).toFixed(2));
            this._zoomRegions[0].setMagFactor(magFactor, magFactor);
        }
    }

    _updateLensMode() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length)
            this._zoomRegions[0].setLensMode(this._settings.get_boolean(LENS_MODE_KEY));
    }

    _updateClampMode() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            this._zoomRegions[0].setClampScrollingAtEdges(
                !this._settings.get_boolean(CLAMP_MODE_KEY));
        }
    }

    _updateMouseTrackingMode() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            this._zoomRegions[0].setMouseTrackingMode(
                this._settings.get_enum(MOUSE_TRACKING_KEY));
        }
    }

    _updateFocusTrackingMode() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            this._zoomRegions[0].setFocusTrackingMode(
                this._settings.get_enum(FOCUS_TRACKING_KEY));
        }
    }

    _updateCaretTrackingMode() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            this._zoomRegions[0].setCaretTrackingMode(
                this._settings.get_enum(CARET_TRACKING_KEY));
        }
    }

    _updateInvertLightness() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            this._zoomRegions[0].setInvertLightness(
                this._settings.get_boolean(INVERT_LIGHTNESS_KEY));
        }
    }

    _updateColorSaturation() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            this._zoomRegions[0].setColorSaturation(
                this._settings.get_double(COLOR_SATURATION_KEY));
        }
    }

    _updateBrightness() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            let brightness = {};
            brightness.r = this._settings.get_double(BRIGHT_RED_KEY);
            brightness.g = this._settings.get_double(BRIGHT_GREEN_KEY);
            brightness.b = this._settings.get_double(BRIGHT_BLUE_KEY);
            this._zoomRegions[0].setBrightness(brightness);
        }
    }

    _updateContrast() {
        // Applies only to the first zoom region.
        if (this._zoomRegions.length) {
            let contrast = {};
            contrast.r = this._settings.get_double(CONTRAST_RED_KEY);
            contrast.g = this._settings.get_double(CONTRAST_GREEN_KEY);
            contrast.b = this._settings.get_double(CONTRAST_BLUE_KEY);
            this._zoomRegions[0].setContrast(contrast);
        }
    }
}

class ZoomRegion {
    constructor(magnifier, mouseSourceActor) {
        this._magnifier = magnifier;
        this._focusCaretTracker = new FocusCaretTracker.FocusCaretTracker();

        this._mouseTrackingMode = GDesktopEnums.MagnifierMouseTrackingMode.NONE;
        this._focusTrackingMode = GDesktopEnums.MagnifierFocusTrackingMode.NONE;
        this._caretTrackingMode = GDesktopEnums.MagnifierCaretTrackingMode.NONE;
        this._clampScrollingAtEdges = false;
        this._lensMode = false;
        this._screenPosition = GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN;
        this._invertLightness = false;
        this._colorSaturation = 1.0;
        this._brightness = {r: NO_CHANGE, g: NO_CHANGE, b: NO_CHANGE};
        this._contrast = {r: NO_CHANGE, g: NO_CHANGE, b: NO_CHANGE};

        this._magView = null;
        this._background = null;
        this._uiGroupClone = null;
        this._mouseSourceActor = mouseSourceActor;
        this._mouseActor  = null;
        this._crossHairs = null;
        this._crossHairsActor = null;

        this._viewPortX = 0;
        this._viewPortY = 0;
        this._viewPortWidth = global.screen_width;
        this._viewPortHeight = global.screen_height;
        this._xCenter = this._viewPortWidth / 2;
        this._yCenter = this._viewPortHeight / 2;
        this._xMagFactor = 1;
        this._yMagFactor = 1;
        this._followingCursor = false;
        this._xFocus = 0;
        this._yFocus = 0;
        this._xCaret = 0;
        this._yCaret = 0;

        this._pointerIdleMonitor = global.backend.get_core_idle_monitor();
        this._scrollContentsTimerId = 0;
    }

    _connectSignals() {
        if (this._signalConnections)
            return;

        this._signalConnections = [];
        let id = Main.layoutManager.connect('monitors-changed',
            this._monitorsChanged.bind(this));
        this._signalConnections.push([Main.layoutManager, id]);

        id = this._focusCaretTracker.connect('caret-moved', this._updateCaret.bind(this));
        this._signalConnections.push([this._focusCaretTracker, id]);

        id = this._focusCaretTracker.connect('focus-changed', this._updateFocus.bind(this));
        this._signalConnections.push([this._focusCaretTracker, id]);
    }

    _disconnectSignals() {
        for (let [obj, id] of this._signalConnections)
            obj.disconnect(id);

        delete this._signalConnections;
    }

    _updateScreenPosition() {
        if (this._screenPosition === GDesktopEnums.MagnifierScreenPosition.NONE) {
            this._setViewPort({
                x: this._viewPortX,
                y: this._viewPortY,
                width: this._viewPortWidth,
                height: this._viewPortHeight,
            });
        } else {
            this.setScreenPosition(this._screenPosition);
        }
    }

    _convertExtentsToScreenSpace(accessible, extents) {
        const toplevelWindowTypes = new Set([
            Atspi.Role.FRAME,
            Atspi.Role.DIALOG,
            Atspi.Role.WINDOW,
        ]);

        try {
            let app = null;
            let parentWindow = null;
            let iter = accessible;
            while (iter) {
                if (iter.get_role() === Atspi.Role.APPLICATION) {
                    app = iter;
                    /* This is the last Accessible we are interested in */
                    break;
                } else if (toplevelWindowTypes.has(iter.get_role())) {
                    parentWindow = iter;
                }
                iter = iter.get_parent();
            }

            /* We don't want to translate our own events to the focus window.
             * They are also already scaled by clutter before being sent, so
             * we don't need to do that here either. */
            if (app && app.get_name() === 'gnome-shell')
                return extents;

            /* Only events from the focused widget of the focused window. Some
             * widgets seem to claim to have focus when the window does not so
             * check both. */
            const windowActive = parentWindow &&
                parentWindow.get_state_set().contains(Atspi.StateType.ACTIVE);
            const accessibleFocused =
                accessible.get_state_set().contains(Atspi.StateType.FOCUSED);
            if (!windowActive || !accessibleFocused)
                return null;
        } catch (e) {
            throw new Error(`Failed to validate parent window: ${e}`);
        }

        const {focusWindow} = global.display;
        if (!focusWindow)
            return null;

        let windowRect = focusWindow.get_frame_rect();
        if (!focusWindow.is_client_decorated())
            windowRect = focusWindow.frame_rect_to_client_rect(windowRect);

        const scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        const screenSpaceExtents = new Atspi.Rect({
            x: windowRect.x + (scaleFactor * extents.x),
            y: windowRect.y + (scaleFactor * extents.y),
            width: scaleFactor * extents.width,
            height: scaleFactor * extents.height,
        });

        return screenSpaceExtents;
    }

    _updateFocus(caller, event) {
        let component = event.source.get_component_iface();
        if (!component || event.detail1 !== 1)
            return;
        let extents;
        try {
            extents = component.get_extents(Atspi.CoordType.WINDOW);
            extents = this._convertExtentsToScreenSpace(event.source, extents);
            if (!extents)
                return;
        } catch (e) {
            log(`Failed to read extents of focused component: ${e.message}`);
            return;
        }

        const [xFocus, yFocus] = [
            extents.x + (extents.width / 2),
            extents.y + (extents.height / 2),
        ];

        if (this._xFocus !== xFocus || this._yFocus !== yFocus) {
            [this._xFocus, this._yFocus] = [xFocus, yFocus];
            this._centerFromFocusPosition();
        }
    }

    _updateCaret(caller, event) {
        let text = event.source.get_text_iface();
        if (!text)
            return;
        let extents;
        try {
            extents = text.get_character_extents(text.get_caret_offset(),
                Atspi.CoordType.WINDOW);
            extents = this._convertExtentsToScreenSpace(text, extents);
            if (!extents)
                return;
        } catch (e) {
            log(`Failed to read extents of text caret: ${e.message}`);
            return;
        }

        const [xCaret, yCaret] = [extents.x, extents.y];

        // Ignore event(s) if the caret size is none (0x0). This happens a lot if
        // the cursor offset can't be translated into a location. This is a work
        // around.
        if (extents.width === 0 && extents.height === 0)
            return;

        if (this._xCaret !== xCaret || this._yCaret !== yCaret) {
            [this._xCaret, this._yCaret] = [xCaret, yCaret];
            this._centerFromCaretPosition();
        }
    }

    /**
     * setActive:
     *
     * @param {boolean} activate Boolean to show/hide the ZoomRegion.
     */
    setActive(activate) {
        if (activate === this.isActive())
            return;

        if (activate) {
            this._createActors();
            if (this._isMouseOverRegion())
                this._magnifier.hideSystemCursor();
            this._updateScreenPosition();
            this._updateMagViewGeometry();
            this._updateCloneGeometry();
            this._updateMousePosition();
            this._connectSignals();
        } else {
            Main.uiGroup.set_opacity(255);
            this._disconnectSignals();
            this._destroyActors();
        }

        this._syncCaretTracking();
        this._syncFocusTracking();
    }

    /**
     * isActive:
     *
     * @returns {boolean} Whether this ZoomRegion is active
     */
    isActive() {
        return this._magView != null;
    }

    /**
     * setMagFactor:
     *
     * @param {number} xMagFactor The power to set the horizontal
     *     magnification factor to of the magnified view. A value of 1.0
     *     means no magnification. A value of 2.0 doubles the size.
     * @param {number} yMagFactor The power to set the vertical
     *     magnification factor to of the magnified view.
     */
    setMagFactor(xMagFactor, yMagFactor) {
        this._changeROI({
            xMagFactor,
            yMagFactor,
            redoCursorTracking: this._followingCursor,
            animate: true,
        });
    }

    /**
     * getMagFactor:
     *
     * @returns {number[]} an array, [xMagFactor, yMagFactor], containing
     *     the horizontal and vertical magnification powers. A value of
     *     1.0 means no magnification. A value of 2.0 means the contents
     *     are doubled in size, and so on.
     */
    getMagFactor() {
        return [this._xMagFactor, this._yMagFactor];
    }

    /**
     * setMouseTrackingMode
     *
     * @param {GDesktopEnums.MagnifierMouseTrackingMode} mode the new mode
     */
    setMouseTrackingMode(mode) {
        if (mode >= GDesktopEnums.MagnifierMouseTrackingMode.NONE &&
            mode <= GDesktopEnums.MagnifierMouseTrackingMode.PUSH)
            this._mouseTrackingMode = mode;
    }

    /**
     * getMouseTrackingMode:
     *
     * @returns {GDesktopEnums.MagnifierMouseTrackingMode} the current mode
     */
    getMouseTrackingMode() {
        return this._mouseTrackingMode;
    }

    /**
     * setFocusTrackingMode
     *
     * @param {GDesktopEnums.MagnifierFocusTrackingMode} mode the new mode
     */
    setFocusTrackingMode(mode) {
        this._focusTrackingMode = mode;
        this._syncFocusTracking();
    }

    /**
     * setCaretTrackingMode
     *
     * @param {GDesktopEnums.MagnifierCaretTrackingMode} mode the new mode
     */
    setCaretTrackingMode(mode) {
        this._caretTrackingMode = mode;
        this._syncCaretTracking();
    }

    _syncFocusTracking() {
        let enabled = this._focusTrackingMode !== GDesktopEnums.MagnifierFocusTrackingMode.NONE &&
            this.isActive();

        if (enabled)
            this._focusCaretTracker.registerFocusListener();
        else
            this._focusCaretTracker.deregisterFocusListener();
    }

    _syncCaretTracking() {
        let enabled = this._caretTrackingMode !== GDesktopEnums.MagnifierCaretTrackingMode.NONE &&
            this.isActive();

        if (enabled)
            this._focusCaretTracker.registerCaretListener();
        else
            this._focusCaretTracker.deregisterCaretListener();
    }

    /**
     * setViewPort
     * Sets the position and size of the ZoomRegion on screen.
     *
     * @param {{x: number, y: number, width: number, height: number}} viewPort
     *     Object defining the position and size of the view port.
     *     The values are in stage coordinate space.
     */
    setViewPort(viewPort) {
        this._setViewPort(viewPort);
        this._screenPosition = GDesktopEnums.MagnifierScreenPosition.NONE;
    }

    /**
     * setROI
     * Sets the "region of interest" that the ZoomRegion is magnifying.
     *
     * @param {{x: number, y: number, width: number, height: number}} roi
     *     Object that defines the region of the screen to magnify.
     *     The values are in screen (unmagnified) coordinate space.
     */
    setROI(roi) {
        if (roi.width <= 0 || roi.height <= 0)
            return;

        this._followingCursor = false;
        this._changeROI({
            xMagFactor: this._viewPortWidth / roi.width,
            yMagFactor: this._viewPortHeight / roi.height,
            xCenter: roi.x + roi.width  / 2,
            yCenter: roi.y + roi.height / 2,
        });
    }

    /**
     * getROI:
     * Retrieves the "region of interest" -- the rectangular bounds of that part
     * of the desktop that the magnified view is showing (x, y, width, height).
     * The bounds are given in non-magnified coordinates.
     *
     * @returns {number[]} an array, [x, y, width, height], representing
     *     the bounding rectangle of what is shown in the magnified view.
     */
    getROI() {
        let roiWidth = this._viewPortWidth / this._xMagFactor;
        let roiHeight = this._viewPortHeight / this._yMagFactor;

        return [
            this._xCenter - roiWidth / 2,
            this._yCenter - roiHeight / 2,
            roiWidth, roiHeight,
        ];
    }

    /**
     * setLensMode:
     *
     *  Turn lens mode on/off.  In full screen mode, lens mode does nothing since
     * a lens the size of the screen is pointless.
     *
     * @param {boolean} lensMode Whether lensMode should be active
     */
    setLensMode(lensMode) {
        this._lensMode = lensMode;
        if (!this._lensMode)
            this.setScreenPosition(this._screenPosition);
    }

    /**
     * isLensMode:
     * Is lens mode on or off?
     *
     * @returns {boolean} The lens mode state.
     */
    isLensMode() {
        return this._lensMode;
    }

    /**
     * setClampScrollingAtEdges:
     * Stop vs. allow scrolling of the magnified contents when it scroll beyond
     * the edges of the screen.
     *
     *  @param {boolean} clamp Boolean to turn on/off clamping.
     */
    setClampScrollingAtEdges(clamp) {
        this._clampScrollingAtEdges = clamp;
        if (clamp)
            this._changeROI();
    }

    /**
     * setTopHalf:
     * Magnifier view occupies the top half of the screen.
     */
    setTopHalf() {
        let viewPort = {};
        viewPort.x = 0;
        viewPort.y = 0;
        viewPort.width = global.screen_width;
        viewPort.height = global.screen_height / 2;
        this._setViewPort(viewPort);
        this._screenPosition = GDesktopEnums.MagnifierScreenPosition.TOP_HALF;
    }

    /**
     * setBottomHalf:
     * Magnifier view occupies the bottom half of the screen.
     */
    setBottomHalf() {
        let viewPort = {};
        viewPort.x = 0;
        viewPort.y = global.screen_height / 2;
        viewPort.width = global.screen_width;
        viewPort.height = global.screen_height / 2;
        this._setViewPort(viewPort);
        this._screenPosition = GDesktopEnums.MagnifierScreenPosition.BOTTOM_HALF;
    }

    /**
     * setLeftHalf:
     * Magnifier view occupies the left half of the screen.
     */
    setLeftHalf() {
        let viewPort = {};
        viewPort.x = 0;
        viewPort.y = 0;
        viewPort.width = global.screen_width / 2;
        viewPort.height = global.screen_height;
        this._setViewPort(viewPort);
        this._screenPosition = GDesktopEnums.MagnifierScreenPosition.LEFT_HALF;
    }

    /**
     * setRightHalf:
     * Magnifier view occupies the right half of the screen.
     */
    setRightHalf() {
        let viewPort = {};
        viewPort.x = global.screen_width / 2;
        viewPort.y = 0;
        viewPort.width = global.screen_width / 2;
        viewPort.height = global.screen_height;
        this._setViewPort(viewPort);
        this._screenPosition = GDesktopEnums.MagnifierScreenPosition.RIGHT_HALF;
    }

    /**
     * setFullScreenMode:
     * Set the ZoomRegion to full-screen mode.
     * Note:  disallows lens mode.
     */
    setFullScreenMode() {
        let viewPort = {};
        viewPort.x = 0;
        viewPort.y = 0;
        viewPort.width = global.screen_width;
        viewPort.height = global.screen_height;
        this.setViewPort(viewPort);

        this._screenPosition = GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN;
    }

    /**
     * setScreenPosition:
     * Positions the zoom region to one of the enumerated positions on the
     * screen.
     *
     *  @param {GDesktopEnums.MagnifierScreenPosition} inPosition the position
     */
    setScreenPosition(inPosition) {
        switch (inPosition) {
        case GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN:
            this.setFullScreenMode();
            break;
        case GDesktopEnums.MagnifierScreenPosition.TOP_HALF:
            this.setTopHalf();
            break;
        case GDesktopEnums.MagnifierScreenPosition.BOTTOM_HALF:
            this.setBottomHalf();
            break;
        case GDesktopEnums.MagnifierScreenPosition.LEFT_HALF:
            this.setLeftHalf();
            break;
        case GDesktopEnums.MagnifierScreenPosition.RIGHT_HALF:
            this.setRightHalf();
            break;
        }
    }

    /**
     * getScreenPosition:
     * Tell the outside world what the current mode is -- magnifiying the
     * top half, bottom half, etc.
     *
     *  @returns {GDesktopEnums.MagnifierScreenPosition}: the current position.
     */
    getScreenPosition() {
        return this._screenPosition;
    }

    _clearScrollContentsTimer() {
        if (this._scrollContentsTimerId !== 0) {
            GLib.source_remove(this._scrollContentsTimerId);
            this._scrollContentsTimerId = 0;
        }
    }

    /**
     * scrollToMousePos:
     * Set the region of interest based on the position of the system pointer.
     *
     *  @returns {boolean}: Whether the system mouse pointer is over the
     *     magnified view.
     */
    scrollToMousePos() {
        this._followingCursor = true;
        if (this._mouseTrackingMode !== GDesktopEnums.MagnifierMouseTrackingMode.NONE)
            this._changeROI({redoCursorTracking: true});
        else
            this._updateMousePosition();

        this._clearScrollContentsTimer();
        this._scrollContentsTimerId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, POINTER_REST_TIME, () => {
            this._followingCursor = false;
            if (this._xDelayed !== null && this._yDelayed !== null) {
                this._scrollContentsToDelayed(this._xDelayed, this._yDelayed);
                this._xDelayed = null;
                this._yDelayed = null;
            }

            this._scrollContentsTimerId = 0;

            return GLib.SOURCE_REMOVE;
        });

        // Determine whether the system mouse pointer is over this zoom region.
        return this._isMouseOverRegion();
    }

    _scrollContentsToDelayed(x, y) {
        if (this._followingCursor) {
            this._xDelayed = x;
            this._yDelayed = y;
        } else {
            this.scrollContentsTo(x, y);
        }
    }

    /**
     * scrollContentsTo:
     * Shift the contents of the magnified view such it is centered on the given
     * coordinate.
     *
     *  @param {number} x The x-coord of the point to center on.
     * @param {number} y The y-coord of the point to center on.
     */
    scrollContentsTo(x, y) {
        if (x < 0 || x > global.screen_width ||
            y < 0 || y > global.screen_height)
            return;

        this._clearScrollContentsTimer();

        this._followingCursor = false;
        this._changeROI({
            xCenter: x,
            yCenter: y,
            animate: true,
        });
    }

    /**
     * addCrosshairs:
     * Add crosshairs centered on the magnified mouse.
     *
     *  @param {Crosshairs} crossHairs Crosshairs instance
     */
    addCrosshairs(crossHairs) {
        this._crossHairs = crossHairs;

        // If the crossHairs is not already within a larger container, add it
        // to this zoom region.  Otherwise, add a clone.
        if (crossHairs && this.isActive())
            this._crossHairsActor = crossHairs.addToZoomRegion(this, this._mouseActor);
    }

    /**
     * setInvertLightness:
     * Set whether to invert the lightness of the magnified view.
     *
     *  @param {boolean} flag whether brightness should be inverted
     */
    setInvertLightness(flag) {
        this._invertLightness = flag;
        if (this._magShaderEffects)
            this._magShaderEffects.setInvertLightness(this._invertLightness);
    }

    /**
     * getInvertLightness:
     *
     * Retrieve whether the lightness is inverted.
     *
     * @returns {boolean} whether brightness should be inverted
     */
    getInvertLightness() {
        return this._invertLightness;
    }

    /**
     * setColorSaturation:
     *
     * Set the color saturation of the magnified view.
     *
     * @param {number} saturation A value from 0.0 to 1.0 that defines
     *     the color saturation, with 0.0 defining no color (grayscale),
     *     and 1.0 defining full color.
     */
    setColorSaturation(saturation) {
        this._colorSaturation = saturation;
        if (this._magShaderEffects)
            this._magShaderEffects.setColorSaturation(this._colorSaturation);
    }

    /**
     * getColorSaturation:
     * Retrieve the color saturation of the magnified view.
     *
     *  @returns {number} the color saturation
     */
    getColorSaturation() {
        return this._colorSaturation;
    }

    /**
     * setBrightness:
     * Alter the brightness of the magnified view.
     *
     *  @param {object} brightness Object containing the contrast for the
     *     red, green, and blue channels. Values of 0.0 represent "standard"
     *     brightness (no change), whereas values less or greater than
     *     0.0 indicate decreased or incresaed brightness, respectively.
     *
     *     {number} brightness.r - the red component
     *     {number} brightness.g - the green component
     *     {number} brightness.b - the blue component
     */
    setBrightness(brightness) {
        this._brightness.r = brightness.r;
        this._brightness.g = brightness.g;
        this._brightness.b = brightness.b;
        if (this._magShaderEffects)
            this._magShaderEffects.setBrightness(this._brightness);
    }

    /**
     * setContrast:
     * Alter the contrast of the magnified view.
     *
     *  @param {object} contrast Object containing the contrast for the
     *     red, green, and blue channels. Values of 0.0 represent "standard"
     *     contrast (no change), whereas values less or greater than
     *     0.0 indicate decreased or incresaed contrast, respectively.
     *
     *     {number} contrast.r - the red component
     *     {number} contrast.g - the green component
     *     {number} contrast.b - the blue component
     */
    setContrast(contrast) {
        this._contrast.r = contrast.r;
        this._contrast.g = contrast.g;
        this._contrast.b = contrast.b;
        if (this._magShaderEffects)
            this._magShaderEffects.setContrast(this._contrast);
    }

    /**
     * getContrast:
     * Retrieve the contrast of the magnified view.
     *
     *  @returns {{r: number, g: number, b: number}}: Object containing
     *     the contrast for the red, green, and blue channels.
     */
    getContrast() {
        let contrast = {};
        contrast.r = this._contrast.r;
        contrast.g = this._contrast.g;
        contrast.b = this._contrast.b;
        return contrast;
    }

    // Private methods //

    _createActors() {
        // Add a group to clip the contents of the magnified view.
        const mainGroup = new Clutter.Actor({clip_to_allocation: true});

        // The root actor for the zoom region
        this._magView = new St.Bin({
            style_class: 'magnifier-zoom-region',
            child: mainGroup,
        });
        global.stage.add_child(this._magView);

        // hide the magnified region from CLUTTER_PICK_ALL
        Shell.util_set_hidden_from_pick(this._magView, true);

        // Add a background for when the magnified uiGroup is scrolled
        // out of view (don't want to see desktop showing through).
        this._background = new Background.SystemBackground();
        mainGroup.add_child(this._background);

        // Clone the group that contains all of UI on the screen.  This is the
        // chrome, the windows, etc.
        this._uiGroupClone = new Clutter.Clone({
            source: Main.uiGroup,
            clip_to_allocation: true,
        });
        mainGroup.add_child(this._uiGroupClone);

        // Add either the given mouseSourceActor to the ZoomRegion, or a clone of
        // it.
        if (this._mouseSourceActor.get_parent() != null)
            this._mouseActor = new Clutter.Clone({source: this._mouseSourceActor});
        else
            this._mouseActor = this._mouseSourceActor;
        mainGroup.add_child(this._mouseActor);

        if (this._crossHairs)
            this._crossHairsActor = this._crossHairs.addToZoomRegion(this, this._mouseActor);
        else
            this._crossHairsActor = null;

        // Contrast and brightness effects.
        this._magShaderEffects = new MagShaderEffects(mainGroup);
        this._magShaderEffects.setColorSaturation(this._colorSaturation);
        this._magShaderEffects.setInvertLightness(this._invertLightness);
        this._magShaderEffects.setBrightness(this._brightness);
        this._magShaderEffects.setContrast(this._contrast);
    }

    _destroyActors() {
        if (this._mouseActor === this._mouseSourceActor)
            this._mouseActor.get_parent().remove_child(this._mouseActor);
        if (this._crossHairs)
            this._crossHairs.removeFromParent(this._crossHairsActor);

        this._magShaderEffects.destroyEffects();
        this._magShaderEffects = null;
        this._magView.destroy();
        this._magView = null;
        this._background = null;
        this._uiGroupClone = null;
        this._mouseActor = null;
        this._crossHairsActor = null;
    }

    _setViewPort(viewPort, fromROIUpdate) {
        // Sets the position of the zoom region on the screen

        let width = Math.round(Math.min(viewPort.width, global.screen_width));
        let height = Math.round(Math.min(viewPort.height, global.screen_height));
        let x = Math.max(viewPort.x, 0);
        let y = Math.max(viewPort.y, 0);

        x = Math.round(Math.min(x, global.screen_width - width));
        y = Math.round(Math.min(y, global.screen_height - height));

        this._viewPortX = x;
        this._viewPortY = y;
        this._viewPortWidth = width;
        this._viewPortHeight = height;

        this._updateMagViewGeometry();

        if (!fromROIUpdate)
            this._changeROI({redoCursorTracking: this._followingCursor}); // will update mouse

        if (this.isActive() && this._isMouseOverRegion())
            this._magnifier.hideSystemCursor();

        const uiGroupIsOccluded = this.isActive() && this._isFullScreen();
        Main.uiGroup.set_opacity(uiGroupIsOccluded ? 0 : 255);
    }

    _changeROI(params) {
        // Updates the area we are viewing; the magnification factors
        // and center can be set explicitly, or we can recompute
        // the position based on the mouse cursor position

        params = Params.parse(params, {
            xMagFactor: this._xMagFactor,
            yMagFactor: this._yMagFactor,
            xCenter: this._xCenter,
            yCenter: this._yCenter,
            redoCursorTracking: false,
            animate: false,
        });

        if (params.xMagFactor <= 0)
            params.xMagFactor = this._xMagFactor;
        if (params.yMagFactor <= 0)
            params.yMagFactor = this._yMagFactor;

        this._xMagFactor = params.xMagFactor;
        this._yMagFactor = params.yMagFactor;

        if (params.redoCursorTracking &&
            this._mouseTrackingMode !== GDesktopEnums.MagnifierMouseTrackingMode.NONE) {
            // This depends on this.xMagFactor/yMagFactor already being updated
            [params.xCenter, params.yCenter] = this._centerFromMousePosition();
        }

        if (this._clampScrollingAtEdges) {
            let roiWidth = this._viewPortWidth / this._xMagFactor;
            let roiHeight = this._viewPortHeight / this._yMagFactor;

            params.xCenter = Math.min(params.xCenter, global.screen_width - roiWidth / 2);
            params.xCenter = Math.max(params.xCenter, roiWidth / 2);
            params.yCenter = Math.min(params.yCenter, global.screen_height - roiHeight / 2);
            params.yCenter = Math.max(params.yCenter, roiHeight / 2);
        }

        this._xCenter = params.xCenter;
        this._yCenter = params.yCenter;

        // If in lens mode, move the magnified view such that it is centered
        // over the actual mouse. However, in full screen mode, the "lens" is
        // the size of the screen -- pointless to move such a large lens around.
        if (this._lensMode && !this._isFullScreen()) {
            this._setViewPort({
                x: this._xCenter - this._viewPortWidth / 2,
                y: this._yCenter - this._viewPortHeight / 2,
                width: this._viewPortWidth,
                height: this._viewPortHeight,
            }, true);
        }

        this._updateCloneGeometry(params.animate);
    }

    _isMouseOverRegion() {
        // Return whether the system mouse sprite is over this ZoomRegion.  If the
        // mouse's position is not given, then it is fetched.
        let mouseIsOver = false;
        if (this.isActive()) {
            let xMouse = this._magnifier.xMouse;
            let yMouse = this._magnifier.yMouse;

            mouseIsOver =
                xMouse >= this._viewPortX && xMouse < (this._viewPortX + this._viewPortWidth) &&
                yMouse >= this._viewPortY && yMouse < (this._viewPortY + this._viewPortHeight);
        }
        return mouseIsOver;
    }

    _isFullScreen() {
        // Does the magnified view occupy the whole screen? Note that this
        // doesn't necessarily imply
        // this._screenPosition = GDesktopEnums.MagnifierScreenPosition.FULL_SCREEN;

        if (this._viewPortX !== 0 || this._viewPortY !== 0)
            return false;
        if (this._viewPortWidth !== global.screen_width ||
            this._viewPortHeight !== global.screen_height)
            return false;
        return true;
    }

    _centerFromMousePosition() {
        // Determines where the center should be given the current cursor
        // position and mouse tracking mode

        let xMouse = this._magnifier.xMouse;
        let yMouse = this._magnifier.yMouse;

        if (this._mouseTrackingMode === GDesktopEnums.MagnifierMouseTrackingMode.PROPORTIONAL)
            return this._centerFromPointProportional(xMouse, yMouse);
        else if (this._mouseTrackingMode === GDesktopEnums.MagnifierMouseTrackingMode.PUSH)
            return this._centerFromPointPush(xMouse, yMouse);
        else if (this._mouseTrackingMode === GDesktopEnums.MagnifierMouseTrackingMode.CENTERED)
            return this._centerFromPointCentered(xMouse, yMouse);

        return null; // Should never be hit
    }

    _centerFromCaretPosition() {
        let xCaret = this._xCaret;
        let yCaret = this._yCaret;

        if (this._caretTrackingMode === GDesktopEnums.MagnifierCaretTrackingMode.PROPORTIONAL)
            [xCaret, yCaret] = this._centerFromPointProportional(xCaret, yCaret);
        else if (this._caretTrackingMode === GDesktopEnums.MagnifierCaretTrackingMode.PUSH)
            [xCaret, yCaret] = this._centerFromPointPush(xCaret, yCaret);
        else if (this._caretTrackingMode === GDesktopEnums.MagnifierCaretTrackingMode.CENTERED)
            [xCaret, yCaret] = this._centerFromPointCentered(xCaret, yCaret);

        this._scrollContentsToDelayed(xCaret, yCaret);
    }

    _centerFromFocusPosition() {
        let xFocus = this._xFocus;
        let yFocus = this._yFocus;

        if (this._focusTrackingMode === GDesktopEnums.MagnifierFocusTrackingMode.PROPORTIONAL)
            [xFocus, yFocus] = this._centerFromPointProportional(xFocus, yFocus);
        else if (this._focusTrackingMode === GDesktopEnums.MagnifierFocusTrackingMode.PUSH)
            [xFocus, yFocus] = this._centerFromPointPush(xFocus, yFocus);
        else if (this._focusTrackingMode === GDesktopEnums.MagnifierFocusTrackingMode.CENTERED)
            [xFocus, yFocus] = this._centerFromPointCentered(xFocus, yFocus);

        this._scrollContentsToDelayed(xFocus, yFocus);
    }

    _centerFromPointPush(xPoint, yPoint) {
        let [xRoi, yRoi, widthRoi, heightRoi] = this.getROI();
        let [cursorWidth, cursorHeight] = this._mouseSourceActor.get_size();
        let xPos = xRoi + widthRoi / 2;
        let yPos = yRoi + heightRoi / 2;
        let xRoiRight = xRoi + widthRoi - cursorWidth;
        let yRoiBottom = yRoi + heightRoi - cursorHeight;

        if (xPoint < xRoi)
            xPos -= xRoi - xPoint;
        else if (xPoint > xRoiRight)
            xPos += xPoint - xRoiRight;

        if (yPoint < yRoi)
            yPos -= yRoi - yPoint;
        else if (yPoint > yRoiBottom)
            yPos += yPoint - yRoiBottom;

        return [xPos, yPos];
    }

    _centerFromPointProportional(xPoint, yPoint) {
        let [xRoi_, yRoi_, widthRoi, heightRoi] = this.getROI();
        let halfScreenWidth = global.screen_width / 2;
        let halfScreenHeight = global.screen_height / 2;
        // We want to pad with a constant distance after zooming, so divide
        // by the magnification factor.
        let unscaledPadding = Math.min(this._viewPortWidth, this._viewPortHeight) / 5;
        let xPadding = unscaledPadding / this._xMagFactor;
        let yPadding = unscaledPadding / this._yMagFactor;
        let xProportion = (xPoint - halfScreenWidth) / halfScreenWidth;   // -1 ... 1
        let yProportion = (yPoint - halfScreenHeight) / halfScreenHeight; // -1 ... 1
        let xPos = xPoint - xProportion * (widthRoi / 2 - xPadding);
        let yPos = yPoint - yProportion * (heightRoi / 2 - yPadding);

        return [xPos, yPos];
    }

    _centerFromPointCentered(xPoint, yPoint) {
        return [xPoint, yPoint];
    }

    _screenToViewPort(screenX, screenY) {
        // Converts coordinates relative to the (unmagnified) screen to coordinates
        // relative to the origin of this._magView
        return [
            this._viewPortWidth / 2 + (screenX - this._xCenter) * this._xMagFactor,
            this._viewPortHeight / 2 + (screenY - this._yCenter) * this._yMagFactor,
        ];
    }

    _updateMagViewGeometry() {
        if (!this.isActive())
            return;

        if (this._isFullScreen())
            this._magView.add_style_class_name('full-screen');
        else
            this._magView.remove_style_class_name('full-screen');

        this._magView.set_size(this._viewPortWidth, this._viewPortHeight);
        this._magView.set_position(this._viewPortX, this._viewPortY);
    }

    _updateCloneGeometry(animate = false) {
        if (!this.isActive())
            return;

        let [x, y] = this._screenToViewPort(0, 0);
        this._uiGroupClone.ease({
            x: Math.round(x),
            y: Math.round(y),
            scale_x: this._xMagFactor,
            scale_y: this._yMagFactor,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            duration: animate ? 100 : 0,
        });

        let [mouseX, mouseY] = this._getMousePosition();
        this._mouseActor.ease({
            x: mouseX,
            y: mouseY,
            scale_x: this._xMagFactor,
            scale_y: this._yMagFactor,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            duration: animate ? 100 : 0,
        });

        if (this._crossHairsActor) {
            let [crossX, crossY] = this._getCrossHairsPosition();
            this._crossHairsActor.ease({
                x: crossX,
                y: crossY,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                duration: animate ? 100 : 0,
            });
        }
    }

    _updateMousePosition() {
        let [xMagMouse, yMagMouse] = this._getMousePosition();
        this._mouseActor.set_position(xMagMouse, yMagMouse);

        if (this._crossHairsActor) {
            let [crossX, crossY] = this._getCrossHairsPosition();
            this._crossHairsActor.set_position(crossX, crossY);
        }
    }

    _getMousePosition() {
        let [xMagMouse, yMagMouse] = this._screenToViewPort(
            this._magnifier.xMouse, this._magnifier.yMouse);
        return [Math.round(xMagMouse), Math.round(yMagMouse)];
    }

    _getCrossHairsPosition() {
        let [xMagMouse, yMagMouse] = this._getMousePosition();
        let [groupWidth, groupHeight] = this._crossHairsActor.get_size();

        return [xMagMouse - groupWidth / 2, yMagMouse - groupHeight / 2];
    }

    _monitorsChanged() {
        this._background.set_size(global.screen_width, global.screen_height);
        this._updateScreenPosition();
    }
}

const Crosshairs = GObject.registerClass(
class Crosshairs extends Clutter.Actor {
    _init() {
        // Set the group containing the crosshairs to three times the desktop
        // size in case the crosshairs need to appear to be infinite in
        // length (i.e., extend beyond the edges of the view they appear in).
        let groupWidth = global.screen_width * 3;
        let groupHeight = global.screen_height * 3;

        super._init({
            clip_to_allocation: false,
            width: groupWidth,
            height: groupHeight,
        });
        this._horizLeftHair = new Clutter.Actor();
        this._horizRightHair = new Clutter.Actor();
        this._vertTopHair = new Clutter.Actor();
        this._vertBottomHair = new Clutter.Actor();
        this.add_child(this._horizLeftHair);
        this.add_child(this._horizRightHair);
        this.add_child(this._vertTopHair);
        this.add_child(this._vertBottomHair);
        this._clipSize = [0, 0];
        this._clones = [];
        this.reCenter();
        this._monitorsChangedId = 0;
    }

    _monitorsChanged() {
        this.set_size(global.screen_width * 3, global.screen_height * 3);
        this.reCenter();
    }

    setEnabled(enabled) {
        if (enabled && this._monitorsChangedId === 0) {
            this._monitorsChangedId = Main.layoutManager.connect(
                'monitors-changed', this._monitorsChanged.bind(this));
        } else if (!enabled && this._monitorsChangedId !== 0) {
            Main.layoutManager.disconnect(this._monitorsChangedId);
            this._monitorsChangedId = 0;
        }
    }

    /**
     * Either add the crosshairs actor to the given ZoomRegion, or, if it is
     * already part of some other ZoomRegion, create a clone of the crosshairs
     * actor, and add the clone instead.  Returns either the original or the
     * clone.
     *
     * @param {ZoomRegion} zoomRegion The container to add the crosshairs
     *     group to.
     * @param {Clutter.Actor} magnifiedMouse The mouse actor for the
     *     zoom region -- used to position the crosshairs and properly
     *     layer them below the mouse.
     * @returns {Clutter.Actor} The crosshairs actor, or its clone.
     */
    addToZoomRegion(zoomRegion, magnifiedMouse) {
        let crosshairsActor = null;
        if (zoomRegion && magnifiedMouse) {
            let container = magnifiedMouse.get_parent();
            if (container) {
                crosshairsActor = this;
                if (this.get_parent() != null) {
                    crosshairsActor = new Clutter.Clone({source: this});
                    this._clones.push(crosshairsActor);

                    // Clones don't share visibility.
                    this.bind_property('visible',
                        crosshairsActor, 'visible',
                        GObject.BindingFlags.SYNC_CREATE);
                }

                container.add_child(crosshairsActor);
                container.set_child_above_sibling(magnifiedMouse, crosshairsActor);
                let [xMouse, yMouse] = magnifiedMouse.get_position();
                let [crosshairsWidth, crosshairsHeight] = crosshairsActor.get_size();
                crosshairsActor.set_position(xMouse - crosshairsWidth / 2, yMouse - crosshairsHeight / 2);
            }
        }
        return crosshairsActor;
    }

    /**
     * removeFromParent:
     *
     * Remove the crosshairs actor from its parent container, or destroy the
     * child actor if it was just a clone of the crosshairs actor.
     *
     * @param {Clutter.Actor} childActor the actor returned from
     *     addToZoomRegion
     */
    removeFromParent(childActor) {
        if (childActor === this)
            childActor.get_parent().remove_child(childActor);
        else
            childActor.destroy();
    }

    /**
     * setColor:
     * Set the color of the crosshairs.
     *
     *  @param {Clutter.Color} clutterColor The color
     */
    setColor(clutterColor) {
        this._horizLeftHair.background_color = clutterColor;
        this._horizRightHair.background_color = clutterColor;
        this._vertTopHair.background_color = clutterColor;
        this._vertBottomHair.background_color = clutterColor;
    }

    /**
     * setThickness:
     *
     * Set the width of the vertical and horizontal lines of the crosshairs.
     *
     * @param {number} thickness the new thickness value
     */
    setThickness(thickness) {
        this._horizLeftHair.set_height(thickness);
        this._horizRightHair.set_height(thickness);
        this._vertTopHair.set_width(thickness);
        this._vertBottomHair.set_width(thickness);
        this.reCenter();
    }

    /**
     * getThickness:
     * Get the width of the vertical and horizontal lines of the crosshairs.
     *
     * @returns {number} The thickness of the crosshairs.
     */
    getThickness() {
        return this._horizLeftHair.get_height();
    }

    /**
     * setOpacity:
     * Set how opaque the crosshairs are.
     *
     * @param {number} opacity Value between 0 (fully transparent)
     *     and 255 (full opaque).
     */
    setOpacity(opacity) {
        // set_opacity() throws an exception for values outside the range
        // [0, 255].
        if (opacity < 0)
            opacity = 0;
        else if (opacity > 255)
            opacity = 255;

        this._horizLeftHair.set_opacity(opacity);
        this._horizRightHair.set_opacity(opacity);
        this._vertTopHair.set_opacity(opacity);
        this._vertBottomHair.set_opacity(opacity);
    }

    /**
     * setLength:
     * Set the length of the vertical and horizontal lines in the crosshairs.
     *
     * @param {number} length The length of the crosshairs.
     */
    setLength(length) {
        this._horizLeftHair.set_width(length);
        this._horizRightHair.set_width(length);
        this._vertTopHair.set_height(length);
        this._vertBottomHair.set_height(length);
        this.reCenter();
    }

    /**
     * getLength:
     * Get the length of the vertical and horizontal lines in the crosshairs.
     *
     * @returns {number} The length of the crosshairs.
     */
    getLength() {
        return this._horizLeftHair.get_width();
    }

    /**
     * setClip:
     * Set the width and height of the rectangle that clips the crosshairs at
     * their intersection
     *
     * @param {[number, number]} size Array of [width, height] defining the size
     *     of the clip rectangle.
     */
    setClip(size) {
        if (size) {
            // Take a chunk out of the crosshairs where it intersects the
            // mouse.
            this._clipSize = size;
            this.reCenter();
        } else {
            // Restore the missing chunk.
            this._clipSize = [0, 0];
            this.reCenter();
        }
    }

    /**
     * reCenter:
     * Reposition the horizontal and vertical hairs such that they cross at
     * the center of crosshairs group.  If called with the dimensions of
     * the clip rectangle, these are used to update the size of the clip.
     *
     * @param {[number, number]} [clipSize] If present, the clip's [width, height].
     */
    reCenter(clipSize) {
        let [groupWidth, groupHeight] = this.get_size();
        let leftLength = this._horizLeftHair.get_width();
        let topLength = this._vertTopHair.get_height();
        let thickness = this._horizLeftHair.get_height();

        // Deal with clip rectangle.
        if (clipSize)
            this._clipSize = clipSize;
        let clipWidth = this._clipSize[0];
        let clipHeight = this._clipSize[1];

        let left = groupWidth / 2 - clipWidth / 2 - leftLength - thickness / 2;
        let right = groupWidth / 2 + clipWidth / 2 + thickness / 2;
        let top = groupHeight / 2 - clipHeight / 2 - topLength - thickness / 2;
        let bottom = groupHeight / 2 + clipHeight / 2 + thickness / 2;
        this._horizLeftHair.set_position(left, (groupHeight - thickness) / 2);
        this._horizRightHair.set_position(right, (groupHeight - thickness) / 2);
        this._vertTopHair.set_position((groupWidth - thickness) / 2, top);
        this._vertBottomHair.set_position((groupWidth - thickness) / 2, bottom);
    }
});

class MagShaderEffects {
    constructor(uiGroupClone) {
        this._inverse = new Shell.InvertLightnessEffect();
        this._brightnessContrast = new Clutter.BrightnessContrastEffect();
        this._colorDesaturation = new Clutter.DesaturateEffect();
        this._inverse.set_enabled(false);
        this._brightnessContrast.set_enabled(false);
        this._colorDesaturation.set_enabled(false);

        this._magView = uiGroupClone;
        this._magView.add_effect(this._inverse);
        this._magView.add_effect(this._brightnessContrast);
        this._magView.add_effect(this._colorDesaturation);
    }

    /**
     * destroyEffects:
     * Remove contrast and brightness effects from the magnified view, and
     * lose the reference to the actor they were applied to.  Don't use this
     * object after calling this.
     */
    destroyEffects() {
        this._magView.clear_effects();
        this._colorDesaturation = null;
        this._brightnessContrast = null;
        this._inverse = null;
        this._magView = null;
    }

    /**
     * setInvertLightness:
     * Enable/disable invert lightness effect.
     *
     * @param {boolean} invertFlag Enabled flag.
     */
    setInvertLightness(invertFlag) {
        this._inverse.set_enabled(invertFlag);
    }

    setColorSaturation(factor) {
        this._colorDesaturation.set_factor(1.0 - factor);
        this._colorDesaturation.set_enabled(factor !== 1.0);
    }

    /**
     * setBrightness:
     * Set the brightness of the magnified view.
     *
     * @param {object} brightness Object containing the contrast for the
     *     red, green, and blue channels. Values of 0.0 represent "standard"
     *     brightness (no change), whereas values less or greater than
     *     0.0 indicate decreased or incresaed brightness, respectively.
     *
     *     {number} brightness.r - the red component
     *     {number} brightness.g - the green component
     *     {number} brightness.b - the blue component
     */
    setBrightness(brightness) {
        let bRed = brightness.r;
        let bGreen = brightness.g;
        let bBlue = brightness.b;
        this._brightnessContrast.set_brightness_full(bRed, bGreen, bBlue);

        // Enable the effect if the brightness OR contrast change are such that
        // it modifies the brightness and/or contrast.
        let [cRed, cGreen, cBlue] = this._brightnessContrast.get_contrast();
        this._brightnessContrast.set_enabled(
            bRed !== NO_CHANGE || bGreen !== NO_CHANGE || bBlue !== NO_CHANGE ||
            cRed !== NO_CHANGE || cGreen !== NO_CHANGE || cBlue !== NO_CHANGE);
    }

    /**
     * Set the contrast of the magnified view.
     *
     * @param {object} contrast Object containing the contrast for the
     *     red, green, and blue channels. Values of 0.0 represent "standard"
     *     contrast (no change), whereas values less or greater than
     *     0.0 indicate decreased or incresaed contrast, respectively.
     *
     *     {number} contrast.r - the red component
     *     {number} contrast.g - the green component
     *     {number} contrast.b - the blue component
     */
    setContrast(contrast) {
        let cRed = contrast.r;
        let cGreen = contrast.g;
        let cBlue = contrast.b;

        this._brightnessContrast.set_contrast_full(cRed, cGreen, cBlue);

        // Enable the effect if the contrast OR brightness change are such that
        // it modifies the brightness and/or contrast.
        // should be able to use Clutter.color_equal(), but that complains of
        // a null first argument.
        let [bRed, bGreen, bBlue] = this._brightnessContrast.get_brightness();
        this._brightnessContrast.set_enabled(
            cRed !== NO_CHANGE || cGreen !== NO_CHANGE || cBlue !== NO_CHANGE ||
            bRed !== NO_CHANGE || bGreen !== NO_CHANGE || bBlue !== NO_CHANGE);
    }
}
(uuay)status/'���X8J�^ G&��tpolkitAgent.js.>// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import AccountsService from 'gi://AccountsService';
import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import PolkitAgent from 'gi://PolkitAgent';
import Polkit from 'gi://Polkit';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Dialog from '../dialog.js';
import * as Main from '../main.js';
import * as ModalDialog from '../modalDialog.js';
import * as ShellEntry from '../shellEntry.js';
import * as UserWidget from '../userWidget.js';
import {wiggle} from '../../misc/animationUtils.js';

/** @enum {number} */
const DialogMode = {
    AUTH: 0,
    CONFIRM: 1,
};

const DIALOG_ICON_SIZE = 64;

const DELAYED_RESET_TIMEOUT = 200;

const AuthenticationDialog = GObject.registerClass({
    Signals: {'done': {param_types: [GObject.TYPE_BOOLEAN]}},
}, class AuthenticationDialog extends ModalDialog.ModalDialog {
    _init(actionId, description, cookie, userNames) {
        super._init({styleClass: 'prompt-dialog'});

        this.actionId = actionId;
        this.message = description;
        this.userNames = userNames;

        Main.sessionMode.connectObject('updated', () => {
            this.visible = !Main.sessionMode.isLocked;
        }, this);

        this.connect('closed', this._onDialogClosed.bind(this));

        let title = _('Authentication Required');

        let headerContent = new Dialog.MessageDialogContent({title, description});
        this.contentLayout.add_child(headerContent);

        let bodyContent = new Dialog.MessageDialogContent();

        if (userNames.length > 1) {
            log(`polkitAuthenticationAgent: Received ${userNames.length} ` +
                'identities that can be used for authentication. Only ' +
                'considering one.');
        }

        let userName = GLib.get_user_name();
        if (!userNames.includes(userName))
            userName = 'root';
        if (!userNames.includes(userName))
            userName = userNames[0];

        this._user = AccountsService.UserManager.get_default().get_user(userName);

        let userBox = new St.BoxLayout({
            style_class: 'polkit-dialog-user-layout',
            vertical: true,
        });
        bodyContent.add_child(userBox);

        this._userAvatar = new UserWidget.Avatar(this._user, {
            iconSize: DIALOG_ICON_SIZE,
        });
        this._userAvatar.x_align = Clutter.ActorAlign.CENTER;
        userBox.add_child(this._userAvatar);

        this._userLabel = new St.Label({
            style_class: userName === 'root'
                ? 'polkit-dialog-user-root-label'
                : 'polkit-dialog-user-label',
        });

        if (userName === 'root')
            this._userLabel.text = _('Administrator');

        userBox.add_child(this._userLabel);

        let passwordBox = new St.BoxLayout({
            style_class: 'prompt-dialog-password-layout',
            vertical: true,
        });

        this._passwordEntry = new St.PasswordEntry({
            style_class: 'prompt-dialog-password-entry',
            text: '',
            can_focus: true,
            visible: false,
            x_align: Clutter.ActorAlign.CENTER,
        });
        ShellEntry.addContextMenu(this._passwordEntry);
        this._passwordEntry.clutter_text.connect('activate', this._onEntryActivate.bind(this));
        this._passwordEntry.bind_property('reactive',
            this._passwordEntry.clutter_text, 'editable',
            GObject.BindingFlags.SYNC_CREATE);
        passwordBox.add_child(this._passwordEntry);

        let warningBox = new St.BoxLayout({vertical: true});

        let capsLockWarning = new ShellEntry.CapsLockWarning();
        this._passwordEntry.bind_property('visible',
            capsLockWarning, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        warningBox.add_child(capsLockWarning);

        this._errorMessageLabel = new St.Label({
            style_class: 'prompt-dialog-error-label',
            visible: false,
        });
        this._errorMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this._errorMessageLabel.clutter_text.line_wrap = true;
        warningBox.add_child(this._errorMessageLabel);

        this._infoMessageLabel = new St.Label({
            style_class: 'prompt-dialog-info-label',
            visible: false,
        });
        this._infoMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this._infoMessageLabel.clutter_text.line_wrap = true;
        warningBox.add_child(this._infoMessageLabel);

        /* text is intentionally non-blank otherwise the height is not the same as for
         * infoMessage and errorMessageLabel - but it is still invisible because
         * gnome-shell.css sets the color to be transparent
         */
        this._nullMessageLabel = new St.Label({style_class: 'prompt-dialog-null-label'});
        this._nullMessageLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this._nullMessageLabel.clutter_text.line_wrap = true;
        warningBox.add_child(this._nullMessageLabel);

        passwordBox.add_child(warningBox);
        bodyContent.add_child(passwordBox);

        this._cancelButton = this.addButton({
            label: _('Cancel'),
            action: this.cancel.bind(this),
            key: Clutter.KEY_Escape,
        });
        this._okButton = this.addButton({
            label: _('Authenticate'),
            action: this._onAuthenticateButtonPressed.bind(this),
            reactive: false,
        });
        this._okButton.bind_property('reactive',
            this._okButton, 'can-focus',
            GObject.BindingFlags.SYNC_CREATE);

        this._passwordEntry.clutter_text.connect('text-changed', text => {
            this._okButton.reactive = text.get_text().length > 0;
        });

        this.contentLayout.add_child(bodyContent);

        this._doneEmitted = false;

        this._mode = -1;

        this._identityToAuth = Polkit.UnixUser.new_for_name(userName);
        this._cookie = cookie;

        this._user.connectObject(
            'notify::is-loaded', this._onUserChanged.bind(this),
            'changed', this._onUserChanged.bind(this), this);
        this._onUserChanged();
    }

    _initiateSession() {
        this._destroySession(DELAYED_RESET_TIMEOUT);

        this._session = new PolkitAgent.Session({
            identity: this._identityToAuth,
            cookie: this._cookie,
        });
        this._session.connectObject(
            'completed', this._onSessionCompleted.bind(this),
            'request', this._onSessionRequest.bind(this),
            'show-error', this._onSessionShowError.bind(this),
            'show-info', this._onSessionShowInfo.bind(this), this);
        this._session.initiate();
    }

    _ensureOpen() {
        // NOTE: ModalDialog.open() is safe to call if the dialog is
        // already open - it just returns true without side-effects
        if (!this.open()) {
            // This can fail if e.g. unable to get input grab
            //
            // In an ideal world this wouldn't happen (because the
            // Shell is in complete control of the session) but that's
            // just not how things work right now.
            //
            // One way to make this happen is by running 'sleep 3;
            // pkexec bash' and then opening a popup menu.
            //
            // We could add retrying if this turns out to be a problem

            log('polkitAuthenticationAgent: Failed to show modal dialog. ' +
                `Dismissing authentication request for action-id ${this.actionId} ` +
                `cookie ${this._cookie}`);
            this._emitDone(true);
        }
    }

    _emitDone(dismissed) {
        if (!this._doneEmitted) {
            this._doneEmitted = true;
            this.emit('done', dismissed);
        }
    }

    _onEntryActivate() {
        let response = this._passwordEntry.get_text();
        if (response.length === 0)
            return;

        this._passwordEntry.reactive = false;
        this._okButton.reactive = false;

        this._session.response(response);
        // When the user responds, dismiss already shown info and
        // error texts (if any)
        this._errorMessageLabel.hide();
        this._infoMessageLabel.hide();
        this._nullMessageLabel.show();
    }

    _onAuthenticateButtonPressed() {
        if (this._mode === DialogMode.CONFIRM)
            this._initiateSession();
        else
            this._onEntryActivate();
    }

    _onSessionCompleted(session, gainedAuthorization) {
        if (this._completed || this._doneEmitted)
            return;

        this._completed = true;

        /* Yay, all done */
        if (gainedAuthorization) {
            this._emitDone(false);
        } else {
            /* Unless we are showing an existing error message from the PAM
             * module (the PAM module could be reporting the authentication
             * error providing authentication-method specific information),
             * show "Sorry, that didn't work. Please try again."
             */
            if (!this._errorMessageLabel.visible) {
                /* Translators: "that didn't work" refers to the fact that the
                 * requested authentication was not gained; this can happen
                 * because of an authentication error (like invalid password),
                 * for instance. */
                this._errorMessageLabel.set_text(_('Sorry, that didn’t work. Please try again.'));
                this._errorMessageLabel.show();
                this._infoMessageLabel.hide();
                this._nullMessageLabel.hide();

                wiggle(this._passwordEntry);
            }

            /* Try and authenticate again */
            this._initiateSession();
        }
    }

    _onSessionRequest(session, request, echoOn) {
        if (this._sessionRequestTimeoutId) {
            GLib.source_remove(this._sessionRequestTimeoutId);
            this._sessionRequestTimeoutId = 0;
        }

        // Hack: The request string comes directly from PAM, if it's "Password:"
        // we replace it with our own to allow localization, if it's something
        // else we remove the last colon and any trailing or leading spaces.
        if (request === 'Password:' || request === 'Password: ')
            this._passwordEntry.hint_text = _('Password');
        else
            this._passwordEntry.hint_text = request.replace(/: *$/, '').trim();

        this._passwordEntry.password_visible = echoOn;

        this._passwordEntry.show();
        this._passwordEntry.set_text('');
        this._passwordEntry.reactive  = true;
        this._okButton.reactive = false;

        this._ensureOpen();
        this._passwordEntry.grab_key_focus();
    }

    _onSessionShowError(session, text) {
        this._passwordEntry.set_text('');
        this._errorMessageLabel.set_text(text);
        this._errorMessageLabel.show();
        this._infoMessageLabel.hide();
        this._nullMessageLabel.hide();
        this._ensureOpen();
    }

    _onSessionShowInfo(session, text) {
        this._passwordEntry.set_text('');
        this._infoMessageLabel.set_text(text);
        this._infoMessageLabel.show();
        this._errorMessageLabel.hide();
        this._nullMessageLabel.hide();
        this._ensureOpen();
    }

    _destroySession(delay = 0) {
        this._session?.disconnectObject(this);

        if (!this._completed)
            this._session?.cancel();

        this._completed = false;
        this._session = null;

        if (this._sessionRequestTimeoutId) {
            GLib.source_remove(this._sessionRequestTimeoutId);
            this._sessionRequestTimeoutId = 0;
        }

        let resetDialog = () => {
            this._sessionRequestTimeoutId = 0;

            if (this.state !== ModalDialog.State.OPENED)
                return GLib.SOURCE_REMOVE;

            this._passwordEntry.hide();
            this._cancelButton.grab_key_focus();
            this._okButton.reactive = false;

            return GLib.SOURCE_REMOVE;
        };

        if (delay) {
            this._sessionRequestTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, resetDialog);
            GLib.Source.set_name_by_id(this._sessionRequestTimeoutId, '[gnome-shell] this._sessionRequestTimeoutId');
        } else {
            resetDialog();
        }
    }

    _onUserChanged() {
        if (!this._user.is_loaded)
            return;

        let userName = this._user.get_user_name();
        let realName = this._user.get_real_name();

        if (userName !== 'root')
            this._userLabel.set_text(realName);

        this._userAvatar.update();

        if (this._user.get_password_mode() === AccountsService.UserPasswordMode.NONE) {
            if (this._mode === DialogMode.CONFIRM)
                return;

            this._mode = DialogMode.CONFIRM;
            this._destroySession();

            this._okButton.reactive = true;

            /* We normally open the dialog when we get a "request" signal, but
             * since in this case initiating a session would perform the
             * authentication, only open the dialog and initiate the session
             * when the user confirmed. */
            this._ensureOpen();
        } else {
            if (this._mode === DialogMode.AUTH)
                return;

            this._mode = DialogMode.AUTH;
            this._initiateSession();
        }
    }

    close(timestamp) {
        // Ensure cleanup if the dialog was never shown
        if (this.state === ModalDialog.State.CLOSED)
            this._onDialogClosed();
        super.close(timestamp);
    }

    cancel() {
        this._emitDone(true);
    }

    _onDialogClosed() {
        Main.sessionMode.disconnectObject(this);

        if (this._sessionRequestTimeoutId)
            GLib.source_remove(this._sessionRequestTimeoutId);
        this._sessionRequestTimeoutId = 0;

        this._user?.disconnectObject(this);
        this._user = null;

        this._destroySession();
    }
});

const AuthenticationAgent = GObject.registerClass(
class AuthenticationAgent extends Shell.PolkitAuthenticationAgent {
    _init() {
        super._init();

        this._currentDialog = null;
        this.connect('initiate', this._onInitiate.bind(this));
        this.connect('cancel', this._onCancel.bind(this));
        this._sessionUpdatedId = 0;
    }

    enable() {
        try {
            this.register();
        } catch (e) {
            log('Failed to register AuthenticationAgent');
        }
    }

    disable() {
        try {
            this.unregister();
        } catch (e) {
            log('Failed to unregister AuthenticationAgent');
        }
    }

    _onInitiate(nativeAgent, actionId, message, iconName, cookie, userNames) {
        // Don't pop up a dialog while locked
        if (Main.sessionMode.isLocked) {
            Main.sessionMode.connectObject('updated', () => {
                Main.sessionMode.disconnectObject(this);

                this._onInitiate(nativeAgent, actionId, message, iconName, cookie, userNames);
            }, this);
            return;
        }

        this._currentDialog = new AuthenticationDialog(actionId, message, cookie, userNames);
        this._currentDialog.connect('done', this._onDialogDone.bind(this));
    }

    _onCancel(_nativeAgent) {
        this._completeRequest(false);
    }

    _onDialogDone(_dialog, dismissed) {
        this._completeRequest(dismissed);
    }

    _completeRequest(dismissed) {
        this._currentDialog.close();
        this._currentDialog = null;

        Main.sessionMode.disconnectObject(this);

        this.complete(dismissed);
    }
});

export {AuthenticationAgent as Component};
(uuay)pageIndicators.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Graphene from 'gi://Graphene';
import GObject from 'gi://GObject';
import St from 'gi://St';

const INDICATOR_INACTIVE_OPACITY = 128;
const INDICATOR_INACTIVE_OPACITY_HOVER = 255;
const INDICATOR_INACTIVE_SCALE = 2 / 3;
const INDICATOR_INACTIVE_SCALE_PRESSED = 0.5;

export const PageIndicators = GObject.registerClass({
    Signals: {'page-activated': {param_types: [GObject.TYPE_INT]}},
}, class PageIndicators extends St.BoxLayout {
    _init(orientation = Clutter.Orientation.VERTICAL) {
        let vertical = orientation === Clutter.Orientation.VERTICAL;
        super._init({
            style_class: 'page-indicators',
            vertical,
            x_expand: true, y_expand: true,
            x_align: vertical ? Clutter.ActorAlign.END : Clutter.ActorAlign.CENTER,
            y_align: vertical ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.END,
            reactive: true,
            clip_to_allocation: true,
        });
        this._nPages = 0;
        this._currentPosition = 0;
        this._reactive = true;
        this._reactive = true;
        this._orientation = orientation;
    }

    vfunc_get_preferred_height(forWidth) {
        // We want to request the natural height of all our children as our
        // natural height, so we chain up to St.BoxLayout, but we only request 0
        // as minimum height, since it's not that important if some indicators
        // are not shown
        let [, natHeight] = super.vfunc_get_preferred_height(forWidth);
        return [0, natHeight];
    }

    setReactive(reactive) {
        let children = this.get_children();
        for (let i = 0; i < children.length; i++)
            children[i].reactive = reactive;

        this._reactive = reactive;
    }

    setNPages(nPages) {
        if (this._nPages === nPages)
            return;

        let diff = nPages - this._nPages;
        if (diff > 0) {
            for (let i = 0; i < diff; i++) {
                let pageIndex = this._nPages + i;
                const indicator = new St.Button({
                    style_class: 'page-indicator',
                    button_mask: St.ButtonMask.ONE |
                                 St.ButtonMask.TWO |
                                 St.ButtonMask.THREE,
                    reactive: this._reactive,
                    child: new St.Widget({
                        style_class: 'page-indicator-icon',
                        pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
                    }),
                });
                indicator.connect('clicked', () => {
                    this.emit('page-activated', pageIndex);
                });
                indicator.connect('notify::hover', () => {
                    this._updateIndicator(indicator, pageIndex);
                });
                indicator.connect('notify::pressed', () => {
                    this._updateIndicator(indicator, pageIndex);
                });
                this._updateIndicator(indicator, pageIndex);
                this.add_child(indicator);
            }
        } else {
            let children = this.get_children().splice(diff);
            for (let i = 0; i < children.length; i++)
                children[i].destroy();
        }
        this._nPages = nPages;
        this.visible = this._nPages > 1;
    }

    _updateIndicator(indicator, pageIndex) {
        let progress =
            Math.max(1 - Math.abs(this._currentPosition - pageIndex), 0);

        let inactiveScale = indicator.pressed
            ? INDICATOR_INACTIVE_SCALE_PRESSED : INDICATOR_INACTIVE_SCALE;
        let inactiveOpacity = indicator.hover
            ? INDICATOR_INACTIVE_OPACITY_HOVER : INDICATOR_INACTIVE_OPACITY;

        let scale = inactiveScale + (1 - inactiveScale) * progress;
        let opacity = inactiveOpacity + (255 - inactiveOpacity) * progress;

        indicator.child.set_scale(scale, scale);
        indicator.child.opacity = opacity;
    }

    setCurrentPosition(currentPosition) {
        this._currentPosition = currentPosition;

        let children = this.get_children();
        for (let i = 0; i < children.length; i++)
            this._updateIndicator(children[i], i);
    }

    get nPages() {
        return this._nPages;
    }
});
(uuay)init.js�import GLib from 'gi://GLib';
import Gio from 'gi://Gio';

import './environment.js';
import {formatError} from '../misc/errorUtils.js';

// Run the Mutter main loop after
// GJS finishes resolving this module.
imports._promiseNative.setMainLoopHook(() => {
    // Queue starting the shell
    GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
        import('./main.js').then(main => main.start()).catch(e => {
            const error = new GLib.Error(
                Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED, formatError(e));
            global.context.terminate_with_error(error);
        });
        return GLib.SOURCE_REMOVE;
    });

    // Run the meta context's main loop
    global.context.run_main_loop();
});
(uuay)kbdA11yDialog.js�import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';

import * as Dialog from './dialog.js';
import * as ModalDialog from './modalDialog.js';

const KEYBOARD_A11Y_SCHEMA    = 'org.gnome.desktop.a11y.keyboard';
const KEY_STICKY_KEYS_ENABLED = 'stickykeys-enable';
const KEY_SLOW_KEYS_ENABLED   = 'slowkeys-enable';

export const KbdA11yDialog = GObject.registerClass(
class KbdA11yDialog extends GObject.Object {
    _init() {
        super._init();

        this._a11ySettings = new Gio.Settings({schema_id: KEYBOARD_A11Y_SCHEMA});

        let seat = Clutter.get_default_backend().get_default_seat();
        seat.connect('kbd-a11y-flags-changed',
            this._showKbdA11yDialog.bind(this));
    }

    _showKbdA11yDialog(seat, newFlags, whatChanged) {
        let dialog = new ModalDialog.ModalDialog();
        let title, description;
        let key, enabled;

        if (whatChanged & Meta.KeyboardA11yFlags.SLOW_KEYS_ENABLED) {
            key = KEY_SLOW_KEYS_ENABLED;
            enabled = (newFlags & Meta.KeyboardA11yFlags.SLOW_KEYS_ENABLED) > 0;
            title = enabled
                ? _('Slow Keys Turned On')
                : _('Slow Keys Turned Off');
            description = _('You just held down the Shift key for 8 seconds. This is the shortcut ' +
                            'for the Slow Keys feature, which affects the way your keyboard works.');
        } else if (whatChanged & Meta.KeyboardA11yFlags.STICKY_KEYS_ENABLED) {
            key = KEY_STICKY_KEYS_ENABLED;
            enabled = (newFlags & Meta.KeyboardA11yFlags.STICKY_KEYS_ENABLED) > 0;
            title = enabled
                ? _('Sticky Keys Turned On')
                : _('Sticky Keys Turned Off');
            description = enabled
                ? _('You just pressed the Shift key 5 times in a row. This is the shortcut ' +
                  'for the Sticky Keys feature, which affects the way your keyboard works.')
                : _('You just pressed two keys at once, or pressed the Shift key 5 times in a row. ' +
                  'This turns off the Sticky Keys feature, which affects the way your keyboard works.');
        } else {
            return;
        }

        let content = new Dialog.MessageDialogContent({title, description});
        dialog.contentLayout.add_child(content);

        dialog.addButton({
            label: enabled ? _('Leave On') : _('Turn On'),
            action: () => {
                this._a11ySettings.set_boolean(key, true);
                dialog.close();
            },
            default: enabled,
            key: !enabled ? Clutter.KEY_Escape : null,
        });

        dialog.addButton({
            label: enabled ? _('Turn Off') : _('Leave Off'),
            action: () => {
                this._a11ySettings.set_boolean(key, false);
                dialog.close();
            },
            default: !enabled,
            key: enabled ? Clutter.KEY_Escape : null,
        });

        dialog.open();
    }
});
(uuay)panelMenu.js^// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import St from 'gi://St';

import * as Main from './main.js';
import * as PopupMenu from './popupMenu.js';

export const ButtonBox = GObject.registerClass(
class ButtonBox extends St.Widget {
    _init(params) {
        super._init({
            style_class: 'panel-button',
            x_expand: true,
            y_expand: true,
            ...params,
        });

        this._delegate = this;

        this.container = new St.Bin({child: this});

        this.connect('style-changed', this._onStyleChanged.bind(this));
        this.connect('destroy', this._onDestroy.bind(this));

        this._minHPadding = this._natHPadding = 0.0;
    }

    _onStyleChanged(actor) {
        let themeNode = actor.get_theme_node();

        this._minHPadding = themeNode.get_length('-minimum-hpadding');
        this._natHPadding = themeNode.get_length('-natural-hpadding');
    }

    vfunc_get_preferred_width(_forHeight) {
        let child = this.get_first_child();
        let minimumSize, naturalSize;

        if (child)
            [minimumSize, naturalSize] = child.get_preferred_width(-1);
        else
            minimumSize = naturalSize = 0;

        minimumSize += 2 * this._minHPadding;
        naturalSize += 2 * this._natHPadding;

        return [minimumSize, naturalSize];
    }

    vfunc_get_preferred_height(_forWidth) {
        let child = this.get_first_child();

        if (child)
            return child.get_preferred_height(-1);

        return [0, 0];
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        let child = this.get_first_child();
        if (!child)
            return;

        let [, natWidth] = child.get_preferred_width(-1);

        let availWidth = box.x2 - box.x1;
        let availHeight = box.y2 - box.y1;

        let childBox = new Clutter.ActorBox();
        if (natWidth + 2 * this._natHPadding <= availWidth) {
            childBox.x1 = this._natHPadding;
            childBox.x2 = availWidth - this._natHPadding;
        } else {
            childBox.x1 = this._minHPadding;
            childBox.x2 = availWidth - this._minHPadding;
        }

        childBox.y1 = 0;
        childBox.y2 = availHeight;

        child.allocate(childBox);
    }

    _onDestroy() {
        this.container.child = null;
        this.container.destroy();
    }
});

export const Button = GObject.registerClass({
    Signals: {'menu-set': {}},
}, class PanelMenuButton extends ButtonBox {
    _init(menuAlignment, nameText, dontCreateMenu) {
        super._init({
            reactive: true,
            can_focus: true,
            track_hover: true,
            accessible_name: nameText ?? '',
            accessible_role: Atk.Role.MENU,
        });

        if (dontCreateMenu)
            this.menu = new PopupMenu.PopupDummyMenu(this);
        else
            this.setMenu(new PopupMenu.PopupMenu(this, menuAlignment, St.Side.TOP, 0));

        this.connect('key-press-event',
            (o, ev) => global.focus_manager.navigate_from_event(ev));
    }

    setSensitive(sensitive) {
        this.reactive = sensitive;
        this.can_focus = sensitive;
        this.track_hover = sensitive;
    }

    setMenu(menu) {
        if (this.menu)
            this.menu.destroy();

        this.menu = menu;
        if (this.menu) {
            this.menu.actor.add_style_class_name('panel-menu');
            this.menu.connect('open-state-changed', this._onOpenStateChanged.bind(this));
            this.menu.actor.connect('key-press-event', this._onMenuKeyPress.bind(this));

            Main.uiGroup.add_child(this.menu.actor);
            this.menu.actor.hide();
        }
        this.emit('menu-set');
    }

    vfunc_event(event) {
        if (this.menu &&
            (event.type() === Clutter.EventType.TOUCH_BEGIN ||
             event.type() === Clutter.EventType.BUTTON_PRESS))
            this.menu.toggle();

        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_hide() {
        super.vfunc_hide();

        if (this.menu)
            this.menu.close();
    }

    _onMenuKeyPress(actor, event) {
        if (global.focus_manager.navigate_from_event(event))
            return Clutter.EVENT_STOP;

        let symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_Left || symbol === Clutter.KEY_Right) {
            let group = global.focus_manager.get_group(this);
            if (group) {
                let direction = symbol === Clutter.KEY_Left ? St.DirectionType.LEFT : St.DirectionType.RIGHT;
                group.navigate_focus(this, direction, false);
                return Clutter.EVENT_STOP;
            }
        }
        return Clutter.EVENT_PROPAGATE;
    }

    _onOpenStateChanged(menu, open) {
        if (open)
            this.add_style_pseudo_class('active');
        else
            this.remove_style_pseudo_class('active');

        // Setting the max-height won't do any good if the minimum height of the
        // menu is higher then the screen; it's useful if part of the menu is
        // scrollable so the minimum height is smaller than the natural height
        let workArea = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex);
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        let verticalMargins = this.menu.actor.margin_top + this.menu.actor.margin_bottom;

        // The workarea and margin dimensions are in physical pixels, but CSS
        // measures are in logical pixels, so make sure to consider the scale
        // factor when computing max-height
        let maxHeight = Math.round((workArea.height - verticalMargins) / scaleFactor);
        this.menu.actor.style = `max-height: ${maxHeight}px;`;
    }

    _onDestroy() {
        if (this.menu)
            this.menu.destroy();
        super._onDestroy();
    }
});
(uuay)batch.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/*
 * Copyright 2011 Red Hat, Inc
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

/*
 * In order for transformation animations to look good, they need to be
 * incremental and have some order to them (e.g., fade out hidden items,
 * then shrink to close the void left over). Chaining animations in this way can
 * be error-prone and wordy using just ease() callbacks.
 *
 * The classes in this file help with this:
 *
 * - Task.  encapsulates schedulable work to be run in a specific scope.
 *
 * - ConsecutiveBatch.  runs a series of tasks in order and completes
 *                      when the last in the series finishes.
 *
 * - ConcurrentBatch.  runs a set of tasks at the same time and completes
 *                     when the last to finish completes.
 *
 * - Hold.  prevents a batch from completing the pending task until
 *          the hold is released.
 *
 * The tasks associated with a batch are specified in a list at batch
 * construction time as either task objects or plain functions.
 * Batches are task objects, themselves, so they can be nested.
 *
 * These classes aren't specific to GDM, but were found to be unintuitive and so
 * are not used elsewhere. These APIs may ultimately get dropped entirely and
 * replaced by something else.
 */

import GObject from 'gi://GObject';
import * as Signals from '../misc/signals.js';

export class Task extends Signals.EventEmitter {
    constructor(scope, handler) {
        super();

        if (scope)
            this.scope = scope;
        else
            this.scope = this;

        this.handler = handler;
    }

    run() {
        if (this.handler)
            return this.handler.call(this.scope);

        return null;
    }
}

export class Hold extends Task {
    constructor() {
        super(null, () => this);

        this._acquisitions = 1;
    }

    acquire() {
        if (this._acquisitions <= 0)
            throw new Error("Cannot acquire hold after it's been released");
        this._acquisitions++;
    }

    acquireUntilAfter(hold) {
        if (!hold.isAcquired())
            return;

        this.acquire();
        let signalId = hold.connect('release', () => {
            hold.disconnect(signalId);
            this.release();
        });
    }

    release() {
        this._acquisitions--;

        if (this._acquisitions === 0)
            this.emit('release');
    }

    isAcquired() {
        return this._acquisitions > 0;
    }
}

export class Batch extends Task {
    constructor(scope, tasks) {
        super();

        this.tasks = [];

        for (let i = 0; i < tasks.length; i++) {
            let task;

            if (tasks[i] instanceof Task)
                task = tasks[i];
            else if (typeof tasks[i] == 'function')
                task = new Task(scope, tasks[i]);
            else
                throw new Error('Batch tasks must be functions or Task, Hold or Batch objects');

            this.tasks.push(task);
        }
    }

    process() {
        throw new GObject.NotImplementedError(`process in ${this.constructor.name}`);
    }

    runTask() {
        if (!(this._currentTaskIndex in this.tasks))
            return null;

        return this.tasks[this._currentTaskIndex].run();
    }

    _finish() {
        this.hold.release();
    }

    nextTask() {
        this._currentTaskIndex++;

        // if the entire batch of tasks is finished, release
        // the hold and notify anyone waiting on the batch
        if (this._currentTaskIndex >= this.tasks.length) {
            this._finish();
            return;
        }

        this.process();
    }

    _start() {
        // acquire a hold to get released when the entire
        // batch of tasks is finished
        this.hold = new Hold();
        this._currentTaskIndex = 0;
        this.process();
    }

    run() {
        this._start();

        // hold may be destroyed at this point
        // if we're already done running
        return this.hold;
    }

    cancel() {
        this.tasks = this.tasks.splice(0, this._currentTaskIndex + 1);
    }
}

export class ConcurrentBatch extends Batch {
    process() {
        let hold = this.runTask();

        if (hold)
            this.hold.acquireUntilAfter(hold);

        // Regardless of the state of the just run task,
        // fire off the next one, so all the tasks can run
        // concurrently.
        this.nextTask();
    }
}

export class ConsecutiveBatch extends Batch {
    process() {
        let hold = this.runTask();

        if (hold && hold.isAcquired()) {
            // This task is inhibiting the batch. Wait on it
            // before processing the next one.
            let signalId = hold.connect('release', () => {
                hold.disconnect(signalId);
                this.nextTask();
            });
        } else {
            // This task finished, process the next one
            this.nextTask();
        }
    }
}
(uuay)volume.js
<// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gvc from 'gi://Gvc';

import * as Main from '../main.js';
import * as PopupMenu from '../popupMenu.js';

import {QuickSlider, SystemIndicator} from '../quickSettings.js';

const ALLOW_AMPLIFIED_VOLUME_KEY = 'allow-volume-above-100-percent';
const UNMUTE_DEFAULT_VOLUME = 0.25;

// Each Gvc.MixerControl is a connection to PulseAudio,
// so it's better to make it a singleton
let _mixerControl;

/**
 * @returns {Gvc.MixerControl} - the mixer control singleton
 */
export function getMixerControl() {
    if (_mixerControl)
        return _mixerControl;

    _mixerControl = new Gvc.MixerControl({name: 'GNOME Shell Volume Control'});
    _mixerControl.open();

    return _mixerControl;
}

const StreamSlider = GObject.registerClass({
    Signals: {
        'stream-updated': {},
    },
}, class StreamSlider extends QuickSlider {
    _init(control) {
        super._init({
            icon_reactive: true,
        });

        this._control = control;

        this._inDrag = false;
        this._notifyVolumeChangeId = 0;

        this._soundSettings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.sound',
        });
        this._soundSettings.connect(`changed::${ALLOW_AMPLIFIED_VOLUME_KEY}`,
            () => this._amplifySettingsChanged());
        this._amplifySettingsChanged();

        this._sliderChangedId = this.slider.connect('notify::value',
            () => this._sliderChanged());
        this.slider.connect('drag-begin', () => (this._inDrag = true));
        this.slider.connect('drag-end', () => {
            this._inDrag = false;
            this._notifyVolumeChange();
        });

        this.connect('icon-clicked', () => {
            if (!this._stream)
                return;

            const {isMuted} = this._stream;
            if (isMuted && this._stream.volume === 0) {
                this._stream.volume =
                    UNMUTE_DEFAULT_VOLUME * this._control.get_vol_max_norm();
                this._stream.push_volume();
            }
            this._stream.change_is_muted(!isMuted);
        });

        this._deviceItems = new Map();

        this._deviceSection = new PopupMenu.PopupMenuSection();
        this.menu.addMenuItem(this._deviceSection);

        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this.menu.addSettingsAction(_('Sound Settings'),
            'gnome-sound-panel.desktop');

        this._stream = null;
        this._volumeCancellable = null;
        this._icons = [];

        this._sync();
    }

    get stream() {
        return this._stream;
    }

    set stream(stream) {
        this._stream?.disconnectObject(this);

        this._stream = stream;

        if (this._stream) {
            this._connectStream(this._stream);
            this._updateVolume();
        } else {
            this.emit('stream-updated');
        }

        this._sync();
    }

    _connectStream(stream) {
        stream.connectObject(
            'notify::is-muted', this._updateVolume.bind(this),
            'notify::volume', this._updateVolume.bind(this), this);
    }

    _lookupDevice(_id) {
        throw new GObject.NotImplementedError(
            `_lookupDevice in ${this.constructor.name}`);
    }

    _activateDevice(_device) {
        throw new GObject.NotImplementedError(
            `_activateDevice in ${this.constructor.name}`);
    }

    _addDevice(id) {
        if (this._deviceItems.has(id))
            return;

        const device = this._lookupDevice(id);
        if (!device)
            return;

        const {description, origin} = device;
        const name = origin
            ? `${description} – ${origin}`
            : description;
        const item = new PopupMenu.PopupImageMenuItem(name, device.get_gicon());
        item.connect('activate', () => {
            const dev = this._lookupDevice(id);
            if (dev)
                this._activateDevice(dev);
            else
                console.warn(`Trying to activate invalid device ${id}`);
        });

        this._deviceSection.addMenuItem(item);
        this._deviceItems.set(id, item);

        this._sync();
    }

    _removeDevice(id) {
        this._deviceItems.get(id)?.destroy();
        if (this._deviceItems.delete(id))
            this._sync();
    }

    _setActiveDevice(activeId) {
        for (const [id, item] of this._deviceItems) {
            item.setOrnament(id === activeId
                ? PopupMenu.Ornament.CHECK
                : PopupMenu.Ornament.NONE);
        }
    }

    _shouldBeVisible() {
        return this._stream != null;
    }

    _sync() {
        this.visible = this._shouldBeVisible();
        this.menuEnabled = this._deviceItems.size > 1;
    }

    _sliderChanged() {
        if (!this._stream)
            return;

        let value = this.slider.value;
        let volume = value * this._control.get_vol_max_norm();
        let prevMuted = this._stream.is_muted;
        let prevVolume = this._stream.volume;
        if (volume < 1) {
            this._stream.volume = 0;
            if (!prevMuted)
                this._stream.change_is_muted(true);
        } else {
            this._stream.volume = volume;
            if (prevMuted)
                this._stream.change_is_muted(false);
        }
        this._stream.push_volume();

        let volumeChanged = this._stream.volume !== prevVolume;
        if (volumeChanged && !this._notifyVolumeChangeId && !this._inDrag) {
            this._notifyVolumeChangeId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 30, () => {
                this._notifyVolumeChange();
                this._notifyVolumeChangeId = 0;
                return GLib.SOURCE_REMOVE;
            });
            GLib.Source.set_name_by_id(this._notifyVolumeChangeId,
                '[gnome-shell] this._notifyVolumeChangeId');
        }
    }

    _notifyVolumeChange() {
        if (this._volumeCancellable)
            this._volumeCancellable.cancel();
        this._volumeCancellable = null;

        if (this._stream.state === Gvc.MixerStreamState.RUNNING)
            return; // feedback not necessary while playing

        this._volumeCancellable = new Gio.Cancellable();
        let player = global.display.get_sound_player();
        player.play_from_theme('audio-volume-change',
            _('Volume changed'), this._volumeCancellable);
    }

    _changeSlider(value) {
        this.slider.block_signal_handler(this._sliderChangedId);
        this.slider.value = value;
        this.slider.unblock_signal_handler(this._sliderChangedId);
    }

    _updateVolume() {
        let muted = this._stream.is_muted;
        this._changeSlider(muted
            ? 0 : this._stream.volume / this._control.get_vol_max_norm());
        this.iconLabel = muted ? _('Unmute') : _('Mute');
        this._updateIcon();
        this.emit('stream-updated');
    }

    _amplifySettingsChanged() {
        this._allowAmplified = this._soundSettings.get_boolean(ALLOW_AMPLIFIED_VOLUME_KEY);

        this.slider.maximum_value = this._allowAmplified
            ? this.getMaxLevel() : 1;

        if (this._stream)
            this._updateVolume();
    }

    _updateIcon() {
        this.iconName = this.getIcon();
    }

    getIcon() {
        if (!this._stream)
            return null;

        let volume = this._stream.volume;
        let n;
        if (this._stream.is_muted || volume <= 0) {
            n = 0;
        } else {
            n = Math.ceil(3 * volume / this._control.get_vol_max_norm());
            n = Math.clamp(n, 1, this._icons.length - 1);
        }
        return this._icons[n];
    }

    getLevel() {
        if (!this._stream)
            return null;

        return this._stream.volume / this._control.get_vol_max_norm();
    }

    getMaxLevel() {
        let maxVolume = this._control.get_vol_max_norm();
        if (this._allowAmplified)
            maxVolume = this._control.get_vol_max_amplified();

        return maxVolume / this._control.get_vol_max_norm();
    }
});

const OutputStreamSlider = GObject.registerClass(
class OutputStreamSlider extends StreamSlider {
    _init(control) {
        super._init(control);

        this.slider.accessible_name = _('Volume');

        this._control.connectObject(
            'output-added', (c, id) => this._addDevice(id),
            'output-removed', (c, id) => this._removeDevice(id),
            'active-output-update', (c, id) => this._setActiveDevice(id),
            this);

        this._icons = [
            'audio-volume-muted-symbolic',
            'audio-volume-low-symbolic',
            'audio-volume-medium-symbolic',
            'audio-volume-high-symbolic',
            'audio-volume-overamplified-symbolic',
        ];

        this.menu.setHeader('audio-headphones-symbolic', _('Sound Output'));
    }

    _connectStream(stream) {
        super._connectStream(stream);
        stream.connectObject('notify::port',
            this._portChanged.bind(this), this);
        this._portChanged();
    }

    _lookupDevice(id) {
        return this._control.lookup_output_id(id);
    }

    _activateDevice(device) {
        this._control.change_output(device);
    }

    _findHeadphones(sink) {
        // This only works for external headphones (e.g. bluetooth)
        if (sink.get_form_factor() === 'headset' ||
            sink.get_form_factor() === 'headphone')
            return true;

        // a bit hackish, but ALSA/PulseAudio have a number
        // of different identifiers for headphones, and I could
        // not find the complete list
        if (sink.get_ports().length > 0)
            return sink.get_port().port.toLowerCase().includes('headphone');

        return false;
    }

    _portChanged() {
        const hasHeadphones = this._findHeadphones(this._stream);
        if (hasHeadphones === this._hasHeadphones)
            return;

        this._hasHeadphones = hasHeadphones;
        this._updateIcon();
    }

    _updateIcon() {
        this.iconName = this._hasHeadphones
            ? 'audio-headphones-symbolic'
            : this.getIcon();
    }
});

const InputStreamSlider = GObject.registerClass(
class InputStreamSlider extends StreamSlider {
    _init(control) {
        super._init(control);

        this.slider.accessible_name = _('Microphone');

        this._control.connectObject(
            'input-added', (c, id) => this._addDevice(id),
            'input-removed', (c, id) => this._removeDevice(id),
            'active-input-update', (c, id) => this._setActiveDevice(id),
            'stream-added', () => this._maybeShowInput(),
            'stream-removed', () => this._maybeShowInput(),
            this);

        this.iconName = 'audio-input-microphone-symbolic';
        this._icons = [
            'microphone-sensitivity-muted-symbolic',
            'microphone-sensitivity-low-symbolic',
            'microphone-sensitivity-medium-symbolic',
            'microphone-sensitivity-high-symbolic',
        ];

        this.menu.setHeader('audio-input-microphone-symbolic', _('Sound Input'));
    }

    _connectStream(stream) {
        super._connectStream(stream);
        this._maybeShowInput();
    }

    _lookupDevice(id) {
        return this._control.lookup_input_id(id);
    }

    _activateDevice(device) {
        this._control.change_input(device);
    }

    _maybeShowInput() {
        // only show input widgets if any application is recording audio
        let showInput = false;
        if (this._stream) {
            // skip gnome-volume-control and pavucontrol which appear
            // as recording because they show the input level
            let skippedApps = [
                'org.gnome.VolumeControl',
                'org.PulseAudio.pavucontrol',
            ];

            showInput = this._control.get_source_outputs().some(
                output => !skippedApps.includes(output.get_application_id()));
        }

        this._showInput = showInput;
        this._sync();
    }

    _shouldBeVisible() {
        return super._shouldBeVisible() && this._showInput;
    }
});

let VolumeIndicator = GObject.registerClass(
class VolumeIndicator extends SystemIndicator {
    constructor() {
        super();

        this._indicator = this._addIndicator();
        this._indicator.reactive = true;
    }

    _handleScrollEvent(item, event) {
        const result = item.slider.scroll(event);
        if (result === Clutter.EVENT_PROPAGATE || item.mapped)
            return result;

        const gicon = new Gio.ThemedIcon({name: item.getIcon()});
        const level = item.getLevel();
        const maxLevel = item.getMaxLevel();
        Main.osdWindowManager.show(-1, gicon, null, level, maxLevel);
        return result;
    }
});

export const OutputIndicator = GObject.registerClass(
class OutputIndicator extends VolumeIndicator {
    constructor() {
        super();

        this._indicator.connect('scroll-event',
            (actor, event) => this._handleScrollEvent(this._output, event));

        this._control = getMixerControl();
        this._control.connectObject(
            'state-changed', () => this._onControlStateChanged(),
            'default-sink-changed', () => this._readOutput(),
            this);

        this._output = new OutputStreamSlider(this._control);
        this._output.connect('stream-updated', () => {
            const icon = this._output.getIcon();

            if (icon)
                this._indicator.icon_name = icon;
            this._indicator.visible = icon !== null;
        });

        this.quickSettingsItems.push(this._output);

        this._onControlStateChanged();
    }

    _onControlStateChanged() {
        if (this._control.get_state() === Gvc.MixerControlState.READY)
            this._readOutput();
        else
            this._indicator.hide();
    }

    _readOutput() {
        this._output.stream = this._control.get_default_sink();
    }
});

export const InputIndicator = GObject.registerClass(
class InputIndicator extends VolumeIndicator {
    constructor() {
        super();

        this._indicator.add_style_class_name('privacy-indicator');

        this._indicator.connect('scroll-event',
            (actor, event) => this._handleScrollEvent(this._input, event));

        this._control = getMixerControl();
        this._control.connectObject(
            'state-changed', () => this._onControlStateChanged(),
            'default-source-changed', () => this._readInput(),
            this);

        this._input = new InputStreamSlider(this._control);
        this._input.connect('stream-updated', () => {
            const icon = this._input.getIcon();

            if (icon)
                this._indicator.icon_name = icon;
        });

        this._input.bind_property('visible',
            this._indicator, 'visible',
            GObject.BindingFlags.SYNC_CREATE);

        this.quickSettingsItems.push(this._input);

        this._onControlStateChanged();
    }

    _onControlStateChanged() {
        if (this._control.get_state() === Gvc.MixerControlState.READY)
            this._readInput();
    }

    _readInput() {
        this._input.stream = this._control.get_default_source();
    }
});
(uuay)extension.jsd
const Gi = imports._gi;

import {ExtensionBase, GettextWrapper} from './sharedInternals.js';

import {extensionManager} from '../ui/main.js';

export class Extension extends ExtensionBase {
    static lookupByUUID(uuid) {
        return extensionManager.lookup(uuid)?.stateObj ?? null;
    }

    static defineTranslationFunctions(url) {
        const wrapper = new GettextWrapper(this, url);
        return wrapper.defineTranslationFunctions();
    }

    enable() {
    }

    disable() {
    }

    /**
     * Open the extension's preferences window
     */
    openPreferences() {
        extensionManager.openExtensionPrefs(this.uuid, '', {});
    }
}

export const {
    gettext, ngettext, pgettext,
} = Extension.defineTranslationFunctions();

export class InjectionManager {
    #savedMethods = new Map();

    /**
     * @callback CreateOverrideFunc
     * @param {Function?} originalMethod - the original method if it exists
     * @returns {Function} - a function to be used as override
     */

    /**
     * Modify, replace or inject a method
     *
     * @param {object} prototype - the object (or prototype) that is modified
     * @param {string} methodName - the name of the overwritten method
     * @param {CreateOverrideFunc} createOverrideFunc - function to call to create the override
     */
    overrideMethod(prototype, methodName, createOverrideFunc) {
        const originalMethod = this._saveMethod(prototype, methodName);
        this._installMethod(prototype, methodName, createOverrideFunc(originalMethod));
    }

    /**
     * Restore the original method
     *
     * @param {object} prototype - the object (or prototype) that is modified
     * @param {string} methodName - the name of the method to restore
     */
    restoreMethod(prototype, methodName) {
        const savedProtoMethods = this.#savedMethods.get(prototype);
        if (!savedProtoMethods)
            return;

        const originalMethod = savedProtoMethods.get(methodName);
        if (originalMethod === undefined)
            delete prototype[methodName];
        else
            this._installMethod(prototype, methodName, originalMethod);

        savedProtoMethods.delete(methodName);
        if (savedProtoMethods.size === 0)
            this.#savedMethods.delete(prototype);
    }

    /**
     * Restore all original methods and clear overrides
     */
    clear() {
        for (const [proto, map] of this.#savedMethods) {
            map.forEach(
                (_, methodName) => this.restoreMethod(proto, methodName));
        }
        console.assert(this.#savedMethods.size === 0,
            `${this.#savedMethods.size} overrides left after clear()`);
    }

    _saveMethod(prototype, methodName) {
        let savedProtoMethods = this.#savedMethods.get(prototype);
        if (!savedProtoMethods) {
            savedProtoMethods = new Map();
            this.#savedMethods.set(prototype, savedProtoMethods);
        }

        const originalMethod = prototype[methodName];
        savedProtoMethods.set(methodName, originalMethod);
        return originalMethod;
    }

    _installMethod(prototype, methodName, method) {
        if (methodName.startsWith('vfunc_')) {
            const giPrototype = prototype[Gi.gobject_prototype_symbol];
            giPrototype[Gi.hook_up_vfunc_symbol](methodName.slice(6), method);
        } else {
            prototype[methodName] = method;
        }
    }
}
(uuay)loginManager.js� // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import * as Signals from './signals.js';

import {loadInterfaceXML} from './fileUtils.js';

const SystemdLoginManagerIface = loadInterfaceXML('org.freedesktop.login1.Manager');
const SystemdLoginSessionIface = loadInterfaceXML('org.freedesktop.login1.Session');
const SystemdLoginUserIface = loadInterfaceXML('org.freedesktop.login1.User');

const SystemdLoginManager = Gio.DBusProxy.makeProxyWrapper(SystemdLoginManagerIface);
const SystemdLoginSession = Gio.DBusProxy.makeProxyWrapper(SystemdLoginSessionIface);
const SystemdLoginUser = Gio.DBusProxy.makeProxyWrapper(SystemdLoginUserIface);

function haveSystemd() {
    return GLib.access('/run/systemd/seats', 0) >= 0;
}

function versionCompare(required, reference) {
    required = required.split('.');
    reference = reference.split('.');

    for (let i = 0; i < required.length; i++) {
        let requiredInt = parseInt(required[i]);
        let referenceInt = parseInt(reference[i]);
        if (requiredInt !== referenceInt)
            return requiredInt < referenceInt;
    }

    return true;
}

/**
 * @returns {boolean}
 */
export function canLock() {
    try {
        let params = GLib.Variant.new('(ss)', ['org.gnome.DisplayManager.Manager', 'Version']);
        let result = Gio.DBus.system.call_sync(
            'org.gnome.DisplayManager',
            '/org/gnome/DisplayManager/Manager',
            'org.freedesktop.DBus.Properties',
            'Get', params, null,
            Gio.DBusCallFlags.NONE,
            -1, null);

        let version = result.deepUnpack()[0].deepUnpack();
        return haveSystemd() && versionCompare('3.5.91', version);
    } catch (e) {
        return false;
    }
}

export async function registerSessionWithGDM() {
    log('Registering session with GDM');
    try {
        await Gio.DBus.system.call(
            'org.gnome.DisplayManager',
            '/org/gnome/DisplayManager/Manager',
            'org.gnome.DisplayManager.Manager',
            'RegisterSession',
            GLib.Variant.new('(a{sv})', [{}]), null,
            Gio.DBusCallFlags.NONE, -1, null);
    } catch (e) {
        if (!e.matches(Gio.DBusError, Gio.DBusError.UNKNOWN_METHOD))
            log(`Error registering session with GDM: ${e.message}`);
        else
            log('Not calling RegisterSession(): method not exported, GDM too old?');
    }
}

let _loginManager = null;

/**
 * An abstraction over systemd/logind and ConsoleKit.
 *
 * @returns {LoginManagerSystemd | LoginManagerDummy} - the LoginManager singleton
 */
export function getLoginManager() {
    if (_loginManager == null) {
        if (haveSystemd())
            _loginManager = new LoginManagerSystemd();
        else
            _loginManager = new LoginManagerDummy();
    }

    return _loginManager;
}

class LoginManagerSystemd extends Signals.EventEmitter {
    constructor() {
        super();

        this._proxy = new SystemdLoginManager(Gio.DBus.system,
            'org.freedesktop.login1',
            '/org/freedesktop/login1');
        this._userProxy = new SystemdLoginUser(Gio.DBus.system,
            'org.freedesktop.login1',
            '/org/freedesktop/login1/user/self');
        this._proxy.connectSignal('PrepareForSleep',
            this._prepareForSleep.bind(this));
        this._proxy.connectSignal('SessionRemoved',
            this._sessionRemoved.bind(this));
    }

    async getCurrentSessionProxy() {
        if (this._currentSession)
            return this._currentSession;

        let sessionId = GLib.getenv('XDG_SESSION_ID');
        if (!sessionId) {
            log('Unset XDG_SESSION_ID, getCurrentSessionProxy() called outside a user session. Asking logind directly.');
            let [session, objectPath] = this._userProxy.Display;
            if (session) {
                log(`Will monitor session ${session}`);
                sessionId = session;
            } else {
                log('Failed to find "Display" session; are we the greeter?');

                for ([session, objectPath] of this._userProxy.Sessions) {
                    let sessionProxy = new SystemdLoginSession(Gio.DBus.system,
                        'org.freedesktop.login1',
                        objectPath);
                    log(`Considering ${session}, class=${sessionProxy.Class}`);
                    if (sessionProxy.Class === 'greeter') {
                        log(`Yes, will monitor session ${session}`);
                        sessionId = session;
                        break;
                    }
                }

                if (!sessionId) {
                    log('No, failed to get session from logind.');
                    return null;
                }
            }
        }

        try {
            const [objectPath] = await this._proxy.GetSessionAsync(sessionId);
            this._currentSession = new SystemdLoginSession(Gio.DBus.system,
                'org.freedesktop.login1', objectPath);
            return this._currentSession;
        } catch (error) {
            logError(error, 'Could not get a proxy for the current session');
            return null;
        }
    }

    async canSuspend() {
        let canSuspend, needsAuth;

        try {
            const [result] = await this._proxy.CanSuspendAsync();
            needsAuth = result === 'challenge';
            canSuspend = needsAuth || result === 'yes';
        } catch (error) {
            canSuspend = false;
            needsAuth = false;
        }
        return {canSuspend, needsAuth};
    }

    async canRebootToBootLoaderMenu() {
        let canRebootToBootLoaderMenu, needsAuth;

        try {
            const [result] = await this._proxy.CanRebootToBootLoaderMenuAsync();
            needsAuth = result === 'challenge';
            canRebootToBootLoaderMenu = needsAuth || result === 'yes';
        } catch (error) {
            canRebootToBootLoaderMenu = false;
            needsAuth = false;
        }
        return {canRebootToBootLoaderMenu, needsAuth};
    }

    setRebootToBootLoaderMenu() {
        /* Parameter is timeout in usec, show to menu for 60 seconds */
        this._proxy.SetRebootToBootLoaderMenuAsync(60000000);
    }

    async listSessions() {
        try {
            const [sessions] = await this._proxy.ListSessionsAsync();
            return sessions;
        } catch (e) {
            return [];
        }
    }

    getSession(objectPath) {
        return new SystemdLoginSession(Gio.DBus.system, 'org.freedesktop.login1', objectPath);
    }

    suspend() {
        this._proxy.SuspendAsync(true);
    }

    async inhibit(reason, cancellable) {
        const inVariant = new GLib.Variant('(ssss)',
            ['sleep', 'GNOME Shell', reason, 'delay']);
        const [outVariant_, fdList] =
            await this._proxy.call_with_unix_fd_list('Inhibit',
                inVariant, 0, -1, null, cancellable);
        const [fd] = fdList.steal_fds();
        return new Gio.UnixInputStream({fd});
    }

    _prepareForSleep(proxy, sender, [aboutToSuspend]) {
        this.emit('prepare-for-sleep', aboutToSuspend);
    }

    _sessionRemoved(proxy, sender, [sessionId]) {
        this.emit('session-removed', sessionId);
    }
}

class LoginManagerDummy extends Signals.EventEmitter  {
    getCurrentSessionProxy() {
        // we could return a DummySession object that fakes whatever callers
        // expect (at the time of writing: connect() and connectSignal()
        // methods), but just never settling the promise should be safer
        return new Promise(() => {});
    }

    canSuspend() {
        return new Promise(resolve => resolve({
            canSuspend: false,
            needsAuth: false,
        }));
    }

    canRebootToBootLoaderMenu() {
        return new Promise(resolve => resolve({
            canRebootToBootLoaderMenu: false,
            needsAuth: false,
        }));
    }

    setRebootToBootLoaderMenu() {
    }

    listSessions() {
        return new Promise(resolve => resolve([]));
    }

    getSession(_objectPath) {
        return null;
    }

    suspend() {
        this.emit('prepare-for-sleep', true);
        this.emit('prepare-for-sleep', false);
    }

    /* eslint-disable-next-line require-await */
    async inhibit() {
        return null;
    }
}
(uuay)dateMenu.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GnomeDesktop from 'gi://GnomeDesktop';
import GObject from 'gi://GObject';
import GWeather from 'gi://GWeather';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Main from './main.js';
import * as PanelMenu from './panelMenu.js';
import * as Calendar from './calendar.js';
import * as Weather from '../misc/weather.js';

import {formatDateWithCFormatString, formatTime, clearCachedLocalTimeZone} from '../misc/dateUtils.js';
import {loadInterfaceXML} from '../misc/fileUtils.js';

const NC_ = (context, str) => `${context}\u0004${str}`;
const T_ = Shell.util_translate_time_string;

const MAX_FORECASTS = 5;
const EN_CHAR = '\u2013';

const ClocksIntegrationIface = loadInterfaceXML('org.gnome.Shell.ClocksIntegration');
const ClocksProxy = Gio.DBusProxy.makeProxyWrapper(ClocksIntegrationIface);

/**
 * @private
 *
 * @param {Date} date a Date
 * @returns {boolean}
 */
function _isToday(date) {
    let now = new Date();
    return now.getFullYear() === date.getFullYear() &&
           now.getMonth() === date.getMonth() &&
           now.getDate() === date.getDate();
}

/**
 * @private
 *
 * @param {GLib.DateTime} datetime a GLib.DateTime
 * @returns {Date}
 */
function _gDateTimeToDate(datetime) {
    return new Date(datetime.to_unix() * 1000 + datetime.get_microsecond() / 1000);
}

const TodayButton = GObject.registerClass(
class TodayButton extends St.Button {
    _init(calendar) {
        // Having the ability to go to the current date if the user is already
        // on the current date can be confusing. So don't make the button reactive
        // until the selected date changes.
        super._init({
            style_class: 'datemenu-today-button',
            x_expand: true,
            can_focus: true,
            reactive: false,
        });

        const hbox = new St.BoxLayout({
            vertical: true,
            x_expand: true,
        });
        this.child = hbox;

        this._dayLabel = new St.Label({
            style_class: 'day-label',
            x_align: Clutter.ActorAlign.START,
        });
        hbox.add_child(this._dayLabel);

        this._dateLabel = new St.Label({style_class: 'date-label'});
        hbox.add_child(this._dateLabel);

        this._calendar = calendar;
        this._calendar.connect('selected-date-changed', (_calendar, datetime) => {
            // Make the button reactive only if the selected date is not the
            // current date.
            this.reactive = !_isToday(_gDateTimeToDate(datetime));
        });
    }

    vfunc_clicked() {
        this._calendar.setDate(new Date(), false);
    }

    setDate(date) {
        this._dayLabel.set_text(formatDateWithCFormatString(date, '%A'));

        /* Translators: This is the date format to use when the calendar popup is
         * shown - it is shown just below the time in the top bar (e.g.,
         * "Tue 9:29 AM").  The string itself should become a full date, e.g.,
         * "February 17 2015".
         */
        const dateFormat = Shell.util_translate_time_string(N_('%B %-d %Y'));
        this._dateLabel.set_text(formatDateWithCFormatString(date, dateFormat));

        /* Translators: This is the accessible name of the date button shown
         * below the time in the shell; it should combine the weekday and the
         * date, e.g. "Tuesday February 17 2015".
         */
        const dateAccessibleNameFormat = Shell.util_translate_time_string(N_('%A %B %e %Y'));
        this.accessible_name = formatDateWithCFormatString(date, dateAccessibleNameFormat);
    }
});

const EventsSection = GObject.registerClass(
class EventsSection extends St.Button {
    _init() {
        super._init({
            style_class: 'events-button',
            can_focus: true,
            x_expand: true,
            child: new St.BoxLayout({
                style_class: 'events-box',
                vertical: true,
                x_expand: true,
            }),
        });

        this._startDate = null;
        this._endDate = null;

        this._eventSource = null;
        this._calendarApp = null;

        this._title = new St.Label({
            style_class: 'events-title',
        });
        this.child.add_child(this._title);

        this._eventsList = new St.BoxLayout({
            style_class: 'events-list',
            vertical: true,
            x_expand: true,
        });
        this.child.add_child(this._eventsList);

        this._appSys = Shell.AppSystem.get_default();
        this._appSys.connect('installed-changed',
            this._appInstalledChanged.bind(this));
        this._appInstalledChanged();
    }

    setDate(date) {
        this._startDate =
            new Date(date.getFullYear(), date.getMonth(), date.getDate());
        this._endDate =
            new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);

        this._updateTitle();
        this._reloadEvents();
    }

    setEventSource(eventSource) {
        if (!(eventSource instanceof Calendar.EventSourceBase))
            throw new Error('Event source is not valid type');

        this._eventSource = eventSource;
        this._eventSource.connect('changed', this._reloadEvents.bind(this));
        this._eventSource.connect('notify::has-calendars',
            this._sync.bind(this));
        this._sync();
    }

    _updateTitle() {
        /* Translators: Shown on calendar heading when selected day occurs on current year */
        const sameYearFormat = T_(NC_('calendar heading', '%B %-d'));

        /* Translators: Shown on calendar heading when selected day occurs on different year */
        const otherYearFormat = T_(NC_('calendar heading', '%B %-d %Y'));

        const timeSpanDay = GLib.TIME_SPAN_DAY / 1000;
        const now = new Date();

        if (this._startDate <= now && now < this._endDate)
            this._title.text = _('Today');
        else if (this._endDate <= now && now - this._endDate < timeSpanDay)
            this._title.text = _('Yesterday');
        else if (this._startDate > now && this._startDate - now <= timeSpanDay)
            this._title.text = _('Tomorrow');
        else if (this._startDate.getFullYear() === now.getFullYear())
            this._title.text = formatDateWithCFormatString(this._startDate, sameYearFormat);
        else
            this._title.text = formatDateWithCFormatString(this._startDate, otherYearFormat);
    }

    _isAtMidnight(eventTime) {
        return eventTime.getHours() === 0 && eventTime.getMinutes() === 0 && eventTime.getSeconds() === 0;
    }

    _formatEventTime(event) {
        const eventStart = event.date;
        let eventEnd = event.end;

        const allDay =
            eventStart.getTime() === this._startDate.getTime() && eventEnd.getTime() === this._endDate.getTime();

        const startsBeforeToday = eventStart < this._startDate;
        const endsAfterToday = eventEnd > this._endDate;

        const startTimeOnly = formatTime(eventStart, {timeOnly: true});
        const endTimeOnly = formatTime(eventEnd, {timeOnly: true});

        const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;

        let title;
        if (allDay) {
            /* Translators: Shown in calendar event list for all day events
             * Keep it short, best if you can use less then 10 characters
             */
            title = C_('event list time', 'All Day');
        } else if (startsBeforeToday || endsAfterToday) {
            const now = new Date();
            const thisYear = now.getFullYear();

            const startsAtMidnight = this._isAtMidnight(eventStart);
            const endsAtMidnight = this._isAtMidnight(eventEnd);

            const startYear = eventStart.getFullYear();

            if (endsAtMidnight) {
                eventEnd = new Date(eventEnd);
                eventEnd.setDate(eventEnd.getDate() - 1);
            }

            const endYear = eventEnd.getFullYear();

            let format;
            if (startYear === thisYear && thisYear === endYear)
                /* Translators: Shown in calendar event list as the start/end of events
                 * that only show day and month
                 */
                format = T_(N_('%m/%d'));
            else
                format = '%x';

            const startDateOnly = formatDateWithCFormatString(eventStart, format);
            const endDateOnly = formatDateWithCFormatString(eventEnd, format);

            if (startsAtMidnight && endsAtMidnight)
                title = `${rtl ? endDateOnly : startDateOnly} ${EN_CHAR} ${rtl ? startDateOnly : endDateOnly}`;
            else if (rtl)
                title = `${endTimeOnly} ${endDateOnly} ${EN_CHAR} ${startTimeOnly} ${startDateOnly}`;
            else
                title = `${startDateOnly} ${startTimeOnly} ${EN_CHAR} ${endDateOnly} ${endTimeOnly}`;
        } else if (eventStart === eventEnd) {
            title = startTimeOnly;
        } else {
            title = `${rtl ? endTimeOnly : startTimeOnly} ${EN_CHAR} ${rtl ? startTimeOnly : endTimeOnly}`;
        }

        return title;
    }

    _reloadEvents() {
        if (this._eventSource.isLoading || this._reloading)
            return;

        this._reloading = true;

        [...this._eventsList].forEach(c => c.destroy());

        const events =
            this._eventSource.getEvents(this._startDate, this._endDate);

        for (let event of events) {
            const box = new St.BoxLayout({
                style_class: 'event-box',
                vertical: true,
            });
            box.add_child(new St.Label({
                text: event.summary,
                style_class: 'event-summary',
            }));
            box.add_child(new St.Label({
                text: this._formatEventTime(event),
                style_class: 'event-time',
            }));
            this._eventsList.add_child(box);
        }

        if (this._eventsList.get_n_children() === 0) {
            const placeholder = new St.Label({
                text: _('No Events'),
                style_class: 'event-placeholder',
            });
            this._eventsList.add_child(placeholder);
        }

        this._reloading = false;
        this._sync();
    }

    vfunc_clicked() {
        Main.overview.hide();
        Main.panel.closeCalendar();

        const appInfo = this._calendarApp;
        const context = global.create_app_launch_context(0, -1);
        if (appInfo.get_id() === 'org.gnome.Evolution.desktop')
            appInfo.launch_action('calendar', context);
        else
            appInfo.launch([], context);
    }

    _appInstalledChanged() {
        const apps = Gio.AppInfo.get_recommended_for_type('text/calendar');
        if (apps && (apps.length > 0)) {
            const app = Gio.AppInfo.get_default_for_type('text/calendar', false);
            const defaultInRecommended = apps.some(a => a.equal(app));
            this._calendarApp = defaultInRecommended ? app : apps[0];
        } else {
            this._calendarApp = null;
        }

        return this._sync();
    }

    _sync() {
        this.visible = this._eventSource && this._eventSource.hasCalendars;
        this.reactive = this._calendarApp !== null;
    }
});

const WorldClocksSection = GObject.registerClass(
class WorldClocksSection extends St.Button {
    _init() {
        super._init({
            style_class: 'world-clocks-button',
            can_focus: true,
            x_expand: true,
        });
        this._clock = new GnomeDesktop.WallClock();
        this._clockNotifyId = 0;
        this._tzNotifyId = 0;

        this._locations = [];

        const layout = new Clutter.GridLayout({orientation: Clutter.Orientation.VERTICAL});
        this._grid = new St.Widget({
            style_class: 'world-clocks-grid',
            x_expand: true,
            layout_manager: layout,
        });
        layout.hookup_style(this._grid);

        this.child = this._grid;

        this._clocksApp = null;
        this._clocksProxy = new ClocksProxy(
            Gio.DBus.session,
            'org.gnome.clocks',
            '/org/gnome/clocks',
            this._onProxyReady.bind(this),
            null /* cancellable */,
            Gio.DBusProxyFlags.DO_NOT_AUTO_START | Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES);

        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.shell.world-clocks',
        });
        this._settings.connect('changed', this._clocksChanged.bind(this));
        this._clocksChanged();

        this._appSystem = Shell.AppSystem.get_default();
        this._appSystem.connect('installed-changed',
            this._sync.bind(this));
        this._sync();
    }

    vfunc_clicked() {
        if (this._clocksApp)
            this._clocksApp.activate();

        Main.overview.hide();
        Main.panel.closeCalendar();
    }

    _sync() {
        this._clocksApp = this._appSystem.lookup_app('org.gnome.clocks.desktop');
        this.visible = this._clocksApp != null;
    }

    _clocksChanged() {
        this._grid.destroy_all_children();
        this._locations = [];

        let world = GWeather.Location.get_world();
        let clocks = this._settings.get_value('locations').deepUnpack();
        for (let i = 0; i < clocks.length; i++) {
            let l = world.deserialize(clocks[i]);
            if (l && l.get_timezone() != null)
                this._locations.push({location: l});
        }

        const unixtime = GLib.DateTime.new_now_local().to_unix();
        this._locations.sort((a, b) => {
            const tzA = a.location.get_timezone();
            const tzB = b.location.get_timezone();
            const intA = tzA.find_interval(GLib.TimeType.STANDARD, unixtime);
            const intB = tzB.find_interval(GLib.TimeType.STANDARD, unixtime);
            return tzA.get_offset(intA) - tzB.get_offset(intB);
        });

        let layout = this._grid.layout_manager;
        const title = this._locations.length === 0
            ? _('Add world clocks…')
            : _('World Clocks');
        const header = new St.Label({
            style_class: 'world-clocks-header',
            x_align: Clutter.ActorAlign.START,
            text: title,
        });

        if (this._locations.length === 0)
            header.add_style_class_name('no-world-clocks');
        else
            header.remove_style_class_name('no-world-clocks');

        if (this._grid.text_direction === Clutter.TextDirection.RTL)
            layout.attach(header, 2, 0, 1, 1);
        else
            layout.attach(header, 0, 0, 2, 1);
        this.label_actor = header;

        for (let i = 0; i < this._locations.length; i++) {
            let l = this._locations[i].location;

            let name = l.get_city_name() || l.get_name();
            const label = new St.Label({
                style_class: 'world-clocks-city',
                text: name,
                x_align: Clutter.ActorAlign.START,
                y_align: Clutter.ActorAlign.CENTER,
                x_expand: true,
            });

            const time = new St.Label({style_class: 'world-clocks-time'});

            const tz = new St.Label({
                style_class: 'world-clocks-timezone',
                x_align: Clutter.ActorAlign.END,
                y_align: Clutter.ActorAlign.CENTER,
            });

            time.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
            tz.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;

            if (this._grid.text_direction === Clutter.TextDirection.RTL) {
                layout.attach(tz, 0, i + 1, 1, 1);
                layout.attach(time, 1, i + 1, 1, 1);
                layout.attach(label, 2, i + 1, 1, 1);
            } else {
                layout.attach(label, 0, i + 1, 1, 1);
                layout.attach(time, 1, i + 1, 1, 1);
                layout.attach(tz, 2, i + 1, 1, 1);
            }

            this._locations[i].timeLabel = time;
            this._locations[i].tzLabel = tz;
        }

        if (this._grid.get_n_children() > 1) {
            if (!this._clockNotifyId) {
                this._clockNotifyId =
                    this._clock.connect('notify::clock', this._updateTimeLabels.bind(this));
            }
            if (!this._tzNotifyId) {
                this._tzNotifyId =
                    this._clock.connect('notify::timezone', this._updateTimezoneLabels.bind(this));
            }
            this._updateTimeLabels();
            this._updateTimezoneLabels();
        } else {
            if (this._clockNotifyId)
                this._clock.disconnect(this._clockNotifyId);
            this._clockNotifyId = 0;

            if (this._tzNotifyId)
                this._clock.disconnect(this._tzNotifyId);
            this._tzNotifyId = 0;
        }
    }

    _getTimezoneOffsetAtLocation(location) {
        const tz = location.get_timezone();
        const localOffset = GLib.DateTime.new_now_local().get_utc_offset();
        const utcOffset = GLib.DateTime.new_now(tz).get_utc_offset();
        const offsetCurrentTz = utcOffset - localOffset;
        const offsetHours =
            Math.floor(Math.abs(offsetCurrentTz) / GLib.TIME_SPAN_HOUR);
        const offsetMinutes =
            (Math.abs(offsetCurrentTz) % GLib.TIME_SPAN_HOUR) /
            GLib.TIME_SPAN_MINUTE;

        const prefix = offsetCurrentTz >= 0 ? '+' : '-';
        const text = offsetMinutes === 0
            ? `${prefix}${offsetHours}`
            : `${prefix}${offsetHours}\u2236${offsetMinutes}`;
        return text;
    }

    _updateTimeLabels() {
        for (let i = 0; i < this._locations.length; i++) {
            let l = this._locations[i];
            const now = GLib.DateTime.new_now(l.location.get_timezone());
            l.timeLabel.text = formatTime(now, {timeOnly: true});
        }
    }

    _updateTimezoneLabels() {
        for (let i = 0; i < this._locations.length; i++) {
            let l = this._locations[i];
            l.tzLabel.text = this._getTimezoneOffsetAtLocation(l.location);
        }
    }

    _onProxyReady(proxy, error) {
        if (error) {
            log(`Failed to create GNOME Clocks proxy: ${error}`);
            return;
        }

        this._clocksProxy.connect('g-properties-changed',
            this._onClocksPropertiesChanged.bind(this));
        this._onClocksPropertiesChanged();
    }

    _onClocksPropertiesChanged() {
        if (this._clocksProxy.g_name_owner == null)
            return;

        this._settings.set_value('locations',
            new GLib.Variant('av', this._clocksProxy.Locations));
    }
});

const WeatherSection = GObject.registerClass(
class WeatherSection extends St.Button {
    _init() {
        super._init({
            style_class: 'weather-button',
            can_focus: true,
            x_expand: true,
        });

        this._weatherClient = new Weather.WeatherClient();

        let box = new St.BoxLayout({
            style_class: 'weather-box',
            vertical: true,
            x_expand: true,
        });

        this.child = box;

        let titleBox = new St.BoxLayout({style_class: 'weather-header-box'});
        this._titleLabel = new St.Label({
            style_class: 'weather-header',
            x_align: Clutter.ActorAlign.START,
            x_expand: true,
            y_align: Clutter.ActorAlign.END,
        });
        titleBox.add_child(this._titleLabel);
        box.add_child(titleBox);

        this._titleLocation = new St.Label({
            style_class: 'weather-header location',
            x_align: Clutter.ActorAlign.END,
            y_align: Clutter.ActorAlign.END,
        });
        titleBox.add_child(this._titleLocation);

        let layout = new Clutter.GridLayout({orientation: Clutter.Orientation.VERTICAL});
        this._forecastGrid = new St.Widget({
            style_class: 'weather-grid',
            layout_manager: layout,
        });
        layout.hookup_style(this._forecastGrid);
        box.add_child(this._forecastGrid);

        this._weatherClient.connect('changed', this._sync.bind(this));
        this._sync();
    }

    vfunc_map() {
        this._weatherClient.update();
        super.vfunc_map();
    }

    vfunc_clicked() {
        this._weatherClient.activateApp();

        Main.overview.hide();
        Main.panel.closeCalendar();
    }

    _getInfos() {
        let forecasts = this._weatherClient.info.get_forecast_list();

        let now = GLib.DateTime.new_now_local();
        let current = GLib.DateTime.new_from_unix_local(0);
        let infos = [];
        for (let i = 0; i < forecasts.length; i++) {
            const [valid, timestamp] = forecasts[i].get_value_update();
            if (!valid || timestamp === 0)
                continue;  // 0 means 'never updated'

            const datetime = GLib.DateTime.new_from_unix_local(timestamp);
            if (now.difference(datetime) > 0)
                continue; // Ignore earlier forecasts

            if (datetime.difference(current) < GLib.TIME_SPAN_HOUR)
                continue; // Enforce a minimum interval of 1h

            if (infos.push(forecasts[i]) === MAX_FORECASTS)
                break; // Use a maximum of five forecasts

            current = datetime;
        }
        return infos;
    }

    _addForecasts() {
        let layout = this._forecastGrid.layout_manager;

        let infos = this._getInfos();
        if (this._forecastGrid.text_direction === Clutter.TextDirection.RTL)
            infos.reverse();

        let col = 0;
        infos.forEach(fc => {
            const [valid_, timestamp] = fc.get_value_update();
            let timeStr = formatTime(new Date(timestamp * 1000), {
                timeOnly: true,
                ampm: false,
            });
            const [, tempValue] = fc.get_value_temp(GWeather.TemperatureUnit.DEFAULT);
            const tempPrefix = Math.round(tempValue) >= 0 ? ' ' : '';

            let time = new St.Label({
                style_class: 'weather-forecast-time',
                text: timeStr,
                x_align: Clutter.ActorAlign.CENTER,
            });
            let icon = new St.Icon({
                style_class: 'weather-forecast-icon',
                icon_name: fc.get_symbolic_icon_name(),
                x_align: Clutter.ActorAlign.CENTER,
                x_expand: true,
            });
            let temp = new St.Label({
                style_class: 'weather-forecast-temp',
                text: `${tempPrefix}${Math.round(tempValue)}°`,
                x_align: Clutter.ActorAlign.CENTER,
            });

            temp.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
            time.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;

            layout.attach(time, col, 0, 1, 1);
            layout.attach(icon, col, 1, 1, 1);
            layout.attach(temp, col, 2, 1, 1);
            col++;
        });
    }

    _setStatusLabel(text) {
        let layout = this._forecastGrid.layout_manager;
        let label = new St.Label({text});
        layout.attach(label, 0, 0, 1, 1);
    }

    _findBestLocationName(loc) {
        const locName = loc.get_name();

        if (loc.get_level() === GWeather.LocationLevel.CITY ||
            !loc.has_coords())
            return locName;

        const world = GWeather.Location.get_world();
        const city = world.find_nearest_city(...loc.get_coords());
        const cityName = city.get_name();

        return locName.includes(cityName) ? cityName : locName;
    }

    _updateForecasts() {
        this._forecastGrid.destroy_all_children();

        if (!this._weatherClient.hasLocation)
            return;

        const {info} = this._weatherClient;
        this._titleLocation.text = this._findBestLocationName(info.location);

        if (this._weatherClient.loading) {
            this._setStatusLabel(_('Loading…'));
            return;
        }

        if (info.is_valid()) {
            this._addForecasts();
            return;
        }

        if (info.network_error())
            this._setStatusLabel(_('Go online for weather information'));
        else
            this._setStatusLabel(_('Weather information is currently unavailable'));
    }

    _sync() {
        this.visible = this._weatherClient.available;

        if (!this.visible)
            return;

        if (this._weatherClient.hasLocation)
            this._titleLabel.text = _('Weather');
        else
            this._titleLabel.text = _('Select weather location…');

        if (this._weatherClient.hasLocation)
            this._titleLabel.remove_style_class_name('no-location');
        else
            this._titleLabel.add_style_class_name('no-location');

        this._forecastGrid.visible = this._weatherClient.hasLocation;
        this._titleLocation.visible = this._weatherClient.hasLocation;

        this._updateForecasts();
    }
});

const MessagesIndicator = GObject.registerClass(
class MessagesIndicator extends St.Icon {
    _init() {
        super._init({
            style_class: 'messages-indicator',
            visible: false,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });

        this._sources = [];
        this._count = 0;

        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.notifications',
        });
        this._settings.connect('changed::show-banners', this._sync.bind(this));

        Main.messageTray.connect('source-added', this._onSourceAdded.bind(this));
        Main.messageTray.connect('source-removed', this._onSourceRemoved.bind(this));
        Main.messageTray.connect('queue-changed', this._updateCount.bind(this));

        let sources = Main.messageTray.getSources();
        sources.forEach(source => this._onSourceAdded(null, source));

        this._sync();

        this.connect('destroy', () => {
            this._settings.run_dispose();
            this._settings = null;
        });
    }

    _onSourceAdded(tray, source) {
        source.connect('notify::count', this._updateCount.bind(this));
        this._sources.push(source);
        this._updateCount();
    }

    _onSourceRemoved(tray, source) {
        this._sources.splice(this._sources.indexOf(source), 1);
        this._updateCount();
    }

    _updateCount() {
        let count = 0;
        this._sources.forEach(source => (count += source.unseenCount));
        this._count = count - Main.messageTray.queueCount;

        this._sync();
    }

    _sync() {
        let doNotDisturb = !this._settings.get_boolean('show-banners');
        this.icon_name = doNotDisturb
            ? 'notifications-disabled-symbolic'
            : 'message-indicator-symbolic';
        this.visible = doNotDisturb || this._count > 0;
    }
});

const FreezableBinLayout = GObject.registerClass(
class FreezableBinLayout extends Clutter.BinLayout {
    _init() {
        super._init();

        this._frozen = false;
        this._savedWidth = [NaN, NaN];
        this._savedHeight = [NaN, NaN];
    }

    set frozen(v) {
        if (this._frozen === v)
            return;

        this._frozen = v;
        if (!this._frozen)
            this.layout_changed();
    }

    vfunc_get_preferred_width(container, forHeight) {
        if (!this._frozen || this._savedWidth.some(isNaN))
            return super.vfunc_get_preferred_width(container, forHeight);
        return this._savedWidth;
    }

    vfunc_get_preferred_height(container, forWidth) {
        if (!this._frozen || this._savedHeight.some(isNaN))
            return super.vfunc_get_preferred_height(container, forWidth);
        return this._savedHeight;
    }

    vfunc_allocate(container, allocation) {
        super.vfunc_allocate(container, allocation);

        let [width, height] = allocation.get_size();
        this._savedWidth = [width, width];
        this._savedHeight = [height, height];
    }
});

const CalendarColumnLayout = GObject.registerClass(
class CalendarColumnLayout extends Clutter.BoxLayout {
    _init(actors) {
        super._init({orientation: Clutter.Orientation.VERTICAL});
        this._colActors = actors;
    }

    vfunc_get_preferred_width(container, forHeight) {
        const actors =
            this._colActors.filter(a => a.get_parent() === container);
        if (actors.length === 0)
            return super.vfunc_get_preferred_width(container, forHeight);
        return actors.reduce(([minAcc, natAcc], child) => {
            const [min, nat] = child.get_preferred_width(forHeight);
            return [Math.max(minAcc, min), Math.max(natAcc, nat)];
        }, [0, 0]);
    }
});

export const DateMenuButton = GObject.registerClass(
class DateMenuButton extends PanelMenu.Button {
    _init() {
        let hbox;

        super._init(0.5);

        this._clockDisplay = new St.Label({style_class: 'clock'});
        this._clockDisplay.clutter_text.y_align = Clutter.ActorAlign.CENTER;
        this._clockDisplay.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;

        this._indicator = new MessagesIndicator();

        const indicatorPad = new St.Widget();
        this._indicator.bind_property('visible',
            indicatorPad, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        indicatorPad.add_constraint(new Clutter.BindConstraint({
            source: this._indicator,
            coordinate: Clutter.BindCoordinate.SIZE,
        }));

        let box = new St.BoxLayout({style_class: 'clock-display-box'});
        box.add_child(indicatorPad);
        box.add_child(this._clockDisplay);
        box.add_child(this._indicator);

        this.label_actor = this._clockDisplay;
        this.add_child(box);
        this.add_style_class_name('clock-display');

        let layout = new FreezableBinLayout();
        let bin = new St.Widget({layout_manager: layout});
        // For some minimal compatibility with PopupMenuItem
        bin._delegate = this;
        this.menu.box.add_child(bin);
        this.menu.box.add_style_class_name('datemenu-popover');

        hbox = new St.BoxLayout({name: 'calendarArea'});
        bin.add_child(hbox);

        this._calendar = new Calendar.Calendar();
        this._calendar.connect('selected-date-changed', (_calendar, datetime) => {
            let date = _gDateTimeToDate(datetime);
            layout.frozen = !_isToday(date);
            this._eventsItem.setDate(date);
        });
        this._date = new TodayButton(this._calendar);

        this.menu.connect('open-state-changed', (menu, isOpen) => {
            // Whenever the menu is opened, select today
            if (isOpen) {
                let now = new Date();
                this._calendar.setDate(now);
                this._date.setDate(now);
                this._eventsItem.setDate(now);
            }
        });

        // Fill up the first column
        this._messageList = new Calendar.CalendarMessageList();
        hbox.add_child(this._messageList);

        // Fill up the second column
        const boxLayout = new CalendarColumnLayout([this._calendar, this._date]);
        const vbox = new St.Widget({
            style_class: 'datemenu-calendar-column',
            layout_manager: boxLayout,
        });
        boxLayout.hookup_style(vbox);
        hbox.add_child(vbox);

        vbox.add_child(this._date);
        vbox.add_child(this._calendar);

        const displaysBox = new St.BoxLayout({
            vertical: true,
            x_expand: true,
            style_class: 'datemenu-displays-box',
        });

        this._displaysSection = new St.ScrollView({
            style_class: 'datemenu-displays-section vfade',
            x_expand: true,
            overlay_scrollbars: true,
            vscrollbar_policy: St.PolicyType.EXTERNAL,
            child: displaysBox,
        });
        vbox.add_child(this._displaysSection);

        this._eventsItem = new EventsSection();
        displaysBox.add_child(this._eventsItem);

        this._clocksItem = new WorldClocksSection();
        displaysBox.add_child(this._clocksItem);

        this._weatherItem = new WeatherSection();
        displaysBox.add_child(this._weatherItem);

        // Done with hbox for calendar and event list

        this._clock = new GnomeDesktop.WallClock();
        this._clock.bind_property('clock', this._clockDisplay, 'text', GObject.BindingFlags.SYNC_CREATE);
        this._clock.connect('notify::timezone', this._updateTimeZone.bind(this));

        Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
        this._sessionUpdated();
    }

    _getEventSource() {
        return new Calendar.DBusEventSource();
    }

    _setEventSource(eventSource) {
        if (this._eventSource)
            this._eventSource.destroy();

        this._calendar.setEventSource(eventSource);
        this._eventsItem.setEventSource(eventSource);

        this._eventSource = eventSource;
    }

    _updateTimeZone() {
        clearCachedLocalTimeZone();

        this._calendar.updateTimeZone();
    }

    _sessionUpdated() {
        let eventSource;
        let showEvents = Main.sessionMode.showCalendarEvents;
        if (showEvents)
            eventSource = this._getEventSource();
        else
            eventSource = new Calendar.EmptyEventSource();

        this._setEventSource(eventSource);

        // Displays are not actually expected to launch Settings when activated
        // but the corresponding app (clocks, weather); however we can consider
        // that display-specific settings, so re-use "allowSettings" here ...
        this._displaysSection.visible = Main.sessionMode.allowSettings;
    }
});
(uuay)weather.js�(// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Geoclue from 'gi://Geoclue';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GWeather from 'gi://GWeather';
import Shell from 'gi://Shell';
import * as Signals from './signals.js';

import * as PermissionStore from './permissionStore.js';

import {loadInterfaceXML} from './fileUtils.js';

Gio._promisify(Geoclue.Simple, 'new');

const WeatherIntegrationIface = loadInterfaceXML('org.gnome.Shell.WeatherIntegration');

const WEATHER_BUS_NAME = 'org.gnome.Weather';
const WEATHER_OBJECT_PATH = '/org/gnome/Weather';
const WEATHER_INTEGRATION_IFACE = 'org.gnome.Shell.WeatherIntegration';

const WEATHER_APP_ID = 'org.gnome.Weather.desktop';

// Minimum time between updates to show loading indication
const UPDATE_THRESHOLD = 10 * GLib.TIME_SPAN_MINUTE;

export class WeatherClient extends Signals.EventEmitter {
    constructor() {
        super();

        this._loading = false;
        this._locationValid = false;
        this._lastUpdate = GLib.DateTime.new_from_unix_local(0);

        this._autoLocationRequested = false;
        this._mostRecentLocation = null;

        this._gclueService = null;
        this._gclueStarted = false;
        this._gclueStarting = false;
        this._gclueLocationChangedId = 0;

        this._needsAuth = true;
        this._weatherAuthorized = false;
        this._permStore = new PermissionStore.PermissionStore(async (proxy, error) => {
            if (error) {
                log(`Failed to connect to permissionStore: ${error.message}`);
                return;
            }

            if (this._permStore.g_name_owner == null) {
                // Failed to auto-start, likely because xdg-desktop-portal
                // isn't installed; don't restrict access to location service
                this._weatherAuthorized = true;
                this._updateAutoLocation();
                return;
            }

            let [perms, data] = [{}, null];
            try {
                [perms, data] = await this._permStore.LookupAsync('gnome', 'geolocation');
            } catch (err) {
                log(`Error looking up permission: ${err.message}`);
            }

            const params = ['gnome', 'geolocation', false, data, perms];
            this._onPermStoreChanged(this._permStore, '', params);
        });
        this._permStore.connectSignal('Changed',
            this._onPermStoreChanged.bind(this));

        this._locationSettings = new Gio.Settings({schema_id: 'org.gnome.system.location'});
        this._locationSettings.connect('changed::enabled',
            this._updateAutoLocation.bind(this));

        this._world = GWeather.Location.get_world();

        const providers =
            GWeather.Provider.METAR |
            GWeather.Provider.MET_NO |
            GWeather.Provider.OWM;
        this._weatherInfo = new GWeather.Info({
            application_id: 'org.gnome.Shell',
            contact_info: 'https://gitlab.gnome.org/GNOME/gnome-shell/-/raw/HEAD/gnome-shell.doap',
            enabled_providers: providers,
        });
        this._weatherInfo.connect_after('updated', () => {
            this._lastUpdate = GLib.DateTime.new_now_local();
            this.emit('changed');
        });

        this._weatherApp = null;
        this._weatherProxy = null;

        this._createWeatherProxy();

        this._settings = new Gio.Settings({
            schema_id: 'org.gnome.shell.weather',
        });
        this._settings.connect('changed::automatic-location',
            this._onAutomaticLocationChanged.bind(this));
        this._onAutomaticLocationChanged();
        this._settings.connect('changed::locations',
            this._onLocationsChanged.bind(this));
        this._onLocationsChanged();

        this._appSystem = Shell.AppSystem.get_default();
        this._appSystem.connect('installed-changed',
            this._onInstalledChanged.bind(this));
        this._onInstalledChanged();
    }

    get available() {
        return this._weatherApp != null;
    }

    get loading() {
        return this._loading;
    }

    get hasLocation() {
        return this._locationValid;
    }

    get info() {
        return this._weatherInfo;
    }

    activateApp() {
        if (this._weatherApp)
            this._weatherApp.activate();
    }

    update() {
        if (!this._locationValid)
            return;

        let now = GLib.DateTime.new_now_local();
        // Update without loading indication if the current info is recent enough
        if (this._weatherInfo.is_valid() &&
            now.difference(this._lastUpdate) < UPDATE_THRESHOLD)
            this._weatherInfo.update();
        else
            this._loadInfo();
    }

    get _useAutoLocation() {
        return this._autoLocationRequested &&
               this._locationSettings.get_boolean('enabled') &&
               (!this._needsAuth || this._weatherAuthorized);
    }

    async _createWeatherProxy() {
        const nodeInfo = Gio.DBusNodeInfo.new_for_xml(WeatherIntegrationIface);
        try {
            this._weatherProxy = await Gio.DBusProxy.new(
                Gio.DBus.session,
                Gio.DBusProxyFlags.DO_NOT_AUTO_START | Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES,
                nodeInfo.lookup_interface(WEATHER_INTEGRATION_IFACE),
                WEATHER_BUS_NAME,
                WEATHER_OBJECT_PATH,
                WEATHER_INTEGRATION_IFACE,
                null);
        } catch (e) {
            log(`Failed to create GNOME Weather proxy: ${e}`);
            return;
        }

        this._weatherProxy.connect('g-properties-changed',
            this._onWeatherPropertiesChanged.bind(this));
        this._onWeatherPropertiesChanged();
    }

    _onWeatherPropertiesChanged() {
        if (this._weatherProxy.g_name_owner == null)
            return;

        this._settings.set_boolean('automatic-location',
            this._weatherProxy.AutomaticLocation);
        this._settings.set_value('locations',
            new GLib.Variant('av', this._weatherProxy.Locations));
    }

    _onInstalledChanged() {
        let hadApp = this._weatherApp != null;
        this._weatherApp = this._appSystem.lookup_app(WEATHER_APP_ID);
        let haveApp = this._weatherApp != null;

        if (hadApp !== haveApp)
            this.emit('changed');

        let neededAuth = this._needsAuth;
        this._needsAuth = this._weatherApp === null ||
                          this._weatherApp.app_info.has_key('X-Flatpak');

        if (neededAuth !== this._needsAuth)
            this._updateAutoLocation();
    }

    _loadInfo() {
        let id = this._weatherInfo.connect('updated', () => {
            this._weatherInfo.disconnect(id);
            this._loading = false;
        });

        this._loading = true;
        this.emit('changed');

        this._weatherInfo.update();
    }

    _locationsEqual(loc1, loc2) {
        if (loc1 === loc2)
            return true;

        if (loc1 == null || loc2 == null)
            return false;

        return loc1.equal(loc2);
    }

    _setLocation(location) {
        if (this._locationsEqual(this._weatherInfo.location, location))
            return;

        this._weatherInfo.abort();
        this._weatherInfo.set_location(location);
        this._locationValid = location != null;

        if (location)
            this._loadInfo();
        else
            this.emit('changed');
    }

    _updateLocationMonitoring() {
        if (this._useAutoLocation) {
            if (this._gclueLocationChangedId !== 0 || this._gclueService == null)
                return;

            this._gclueLocationChangedId =
                this._gclueService.connect('notify::location',
                    this._onGClueLocationChanged.bind(this));
            this._onGClueLocationChanged();
        } else {
            if (this._gclueLocationChangedId)
                this._gclueService.disconnect(this._gclueLocationChangedId);
            this._gclueLocationChangedId = 0;
        }
    }

    async _startGClueService() {
        if (this._gclueStarting)
            return;

        this._gclueStarting = true;

        try {
            this._gclueService = await Geoclue.Simple.new(
                'org.gnome.Shell', Geoclue.AccuracyLevel.CITY, null);
        } catch (e) {
            log(`Failed to connect to Geoclue2 service: ${e.message}`);
            this._setLocation(this._mostRecentLocation);
            return;
        }
        this._gclueStarted = true;
        this._gclueService.get_client().distance_threshold = 100;
        this._updateLocationMonitoring();
    }

    _onGClueLocationChanged() {
        let geoLocation = this._gclueService.location;
        // Provide empty name so GWeather sets location name
        const location = GWeather.Location.new_detached('',
            null,
            geoLocation.latitude,
            geoLocation.longitude);
        this._setLocation(location);
    }

    _onAutomaticLocationChanged() {
        let useAutoLocation = this._settings.get_boolean('automatic-location');
        if (this._autoLocationRequested === useAutoLocation)
            return;

        this._autoLocationRequested = useAutoLocation;

        this._updateAutoLocation();
    }

    _updateAutoLocation() {
        this._updateLocationMonitoring();

        if (this._useAutoLocation)
            this._startGClueService();
        else
            this._setLocation(this._mostRecentLocation);
    }

    _onLocationsChanged() {
        let locations = this._settings.get_value('locations').deepUnpack();
        let serialized = locations.shift();
        let mostRecentLocation = null;

        if (serialized)
            mostRecentLocation = this._world.deserialize(serialized);

        if (this._locationsEqual(this._mostRecentLocation, mostRecentLocation))
            return;

        this._mostRecentLocation = mostRecentLocation;

        if (!this._useAutoLocation || !this._gclueStarted)
            this._setLocation(this._mostRecentLocation);
    }

    _onPermStoreChanged(proxy, sender, params) {
        let [table, id, deleted_, data_, perms] = params;

        if (table !== 'gnome' || id !== 'geolocation')
            return;

        let permission = perms['org.gnome.Weather'] || ['NONE'];
        let [accuracy] = permission;
        this._weatherAuthorized = accuracy !== 'NONE';

        this._updateAutoLocation();
    }
}
(uuay)locatePointer.jsO// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import * as Ripples from './ripples.js';
import * as Main from './main.js';

const LOCATE_POINTER_KEY = 'locate-pointer';
const LOCATE_POINTER_SCHEMA = 'org.gnome.desktop.interface';

export class LocatePointer {
    constructor() {
        this._settings = new Gio.Settings({schema_id: LOCATE_POINTER_SCHEMA});
        this._settings.connect(`changed::${LOCATE_POINTER_KEY}`, () => this._syncEnabled());
        this._syncEnabled();
    }

    _syncEnabled() {
        let enabled = this._settings.get_boolean(LOCATE_POINTER_KEY);
        if (enabled === !!this._ripples)
            return;

        if (enabled) {
            this._ripples = new Ripples.Ripples(0.5, 0.5, 'ripple-pointer-location');
            this._ripples.addTo(Main.uiGroup);
        } else {
            this._ripples.destroy();
            this._ripples = null;
        }
    }

    show() {
        if (!this._ripples)
            return;

        let [x, y] = global.get_pointer();
        this._ripples.playAnimation(x, y);
    }
}
(uuay)focusCaretTracker.js7/** -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
 * Copyright 2012 Inclusive Design Research Centre, OCAD University.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Author:
 *   Joseph Scheuhammer <clown@alum.mit.edu>
 * Contributor:
 *   Magdalen Berns <m.berns@sms.ed.ac.uk>
 */

import Atspi from 'gi://Atspi';
import * as Signals from '../misc/signals.js';

const CARETMOVED        = 'object:text-caret-moved';
const STATECHANGED      = 'object:state-changed';

export class FocusCaretTracker extends Signals.EventEmitter {
    constructor() {
        super();

        this._atspiListener = Atspi.EventListener.new(this._onChanged.bind(this));

        this._atspiInited = false;
        this._focusListenerRegistered = false;
        this._caretListenerRegistered = false;
    }

    _onChanged(event) {
        if (event.type.indexOf(STATECHANGED) === 0)
            this.emit('focus-changed', event);
        else if (event.type === CARETMOVED)
            this.emit('caret-moved', event);
    }

    _initAtspi() {
        if (!this._atspiInited && Atspi.init() === 0) {
            Atspi.set_timeout(250, 250);
            this._atspiInited = true;
        }

        return this._atspiInited;
    }

    registerFocusListener() {
        if (!this._initAtspi() || this._focusListenerRegistered)
            return;

        this._atspiListener.register(`${STATECHANGED}:focused`);
        this._atspiListener.register(`${STATECHANGED}:selected`);
        this._focusListenerRegistered = true;
    }

    registerCaretListener() {
        if (!this._initAtspi() || this._caretListenerRegistered)
            return;

        this._atspiListener.register(CARETMOVED);
        this._caretListenerRegistered = true;
    }

    deregisterFocusListener() {
        if (!this._focusListenerRegistered)
            return;

        this._atspiListener.deregister(`${STATECHANGED}:focused`);
        this._atspiListener.deregister(`${STATECHANGED}:selected`);
        this._focusListenerRegistered = false;
    }

    deregisterCaretListener() {
        if (!this._caretListenerRegistered)
            return;

        this._atspiListener.deregister(CARETMOVED);
        this._caretListenerRegistered = false;
    }
}
(uuay)inputMethod.jsU3// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import IBus from 'gi://IBus';

import * as Keyboard from '../ui/status/keyboard.js';
import * as Main from '../ui/main.js';

Gio._promisify(IBus.Bus.prototype,
    'create_input_context_async', 'create_input_context_async_finish');
Gio._promisify(IBus.InputContext.prototype,
    'process_key_event_async', 'process_key_event_async_finish');

const HIDE_PANEL_TIME = 50;

const HAVE_REQUIRE_SURROUNDING_TEXT = GObject.signal_lookup('require-surrounding-text', IBus.InputContext);

export const InputMethod = GObject.registerClass({
    Signals: {
        'surrounding-text-set': {},
    },
}, class InputMethod extends Clutter.InputMethod {
    _init() {
        super._init();
        this._hints = 0;
        this._purpose = 0;
        this._currentFocus = null;
        this._preeditStr = '';
        this._preeditPos = 0;
        this._preeditAnchor = 0;
        this._preeditVisible = false;
        this._hidePanelId = 0;
        this._ibus = IBus.Bus.new_async();
        this._ibus.connect('connected', this._onConnected.bind(this));
        this._ibus.connect('disconnected', this._clear.bind(this));
        this.connect('notify::can-show-preedit', this._updateCapabilities.bind(this));

        this._inputSourceManager = Keyboard.getInputSourceManager();
        this._sourceChangedId = this._inputSourceManager.connect('current-source-changed',
            this._onSourceChanged.bind(this));
        this._currentSource = this._inputSourceManager.currentSource;

        if (this._ibus.is_connected())
            this._onConnected();
    }

    get currentFocus() {
        return this._currentFocus;
    }

    _updateCapabilities() {
        let caps = IBus.Capabilite.PREEDIT_TEXT | IBus.Capabilite.FOCUS | IBus.Capabilite.SURROUNDING_TEXT;

        if (Main.keyboard.visible)
            caps |= IBus.Capabilite.OSK;

        if (this._context)
            this._context.set_capabilities(caps);
    }

    _onSourceChanged() {
        this._currentSource = this._inputSourceManager.currentSource;
    }

    async _onConnected() {
        this._cancellable = new Gio.Cancellable();
        try {
            this._context = await this._ibus.create_input_context_async(
                'gnome-shell', -1, this._cancellable);
        } catch (e) {
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
                logError(e);
                this._clear();
            }
            return;
        }

        this._context.set_client_commit_preedit(true);
        this._context.connect('commit-text', this._onCommitText.bind(this));
        this._context.connect('delete-surrounding-text', this._onDeleteSurroundingText.bind(this));
        this._context.connect('update-preedit-text-with-mode', this._onUpdatePreeditText.bind(this));
        this._context.connect('show-preedit-text', this._onShowPreeditText.bind(this));
        this._context.connect('hide-preedit-text', this._onHidePreeditText.bind(this));
        this._context.connect('forward-key-event', this._onForwardKeyEvent.bind(this));
        this._context.connect('destroy', this._clear.bind(this));

        if (HAVE_REQUIRE_SURROUNDING_TEXT)
            this._context.connect('require-surrounding-text', this._onRequireSurroundingText.bind(this));

        Main.keyboard.connectObject('visibility-changed', () => this._updateCapabilities());

        this._updateCapabilities();
    }

    _clear() {
        Main.keyboard.disconnectObject(this);

        if (this._cancellable) {
            this._cancellable.cancel();
            this._cancellable = null;
        }

        this._context = null;
        this._hints = 0;
        this._purpose = 0;
        this._preeditStr = '';
        this._preeditPos = 0;
        this._preeditAnchor = 0;
        this._preeditVisible = false;
    }

    _emitRequestSurrounding() {
        if (this._context.needs_surrounding_text())
            this.emit('request-surrounding');
    }

    _onCommitText(_context, text) {
        this.commit(text.get_text());
    }

    _onRequireSurroundingText(_context) {
        this.request_surrounding();
    }

    _onDeleteSurroundingText(_context, offset, nchars) {
        try {
            this.delete_surrounding(offset, nchars);
        } catch (e) {
            // We may get out of bounds for negative offset on older mutter
            this.delete_surrounding(0, nchars + offset);
        }
    }

    _onUpdatePreeditText(_context, text, pos, visible, mode) {
        if (text == null)
            return;

        let preedit = text.get_text();
        if (preedit === '')
            preedit = null;

        const anchor = pos;
        if (visible)
            this.set_preedit_text(preedit, pos, anchor, mode);
        else if (this._preeditVisible)
            this.set_preedit_text(null, pos, anchor, mode);

        this._preeditStr = preedit;
        this._preeditPos = pos;
        this._preeditAnchor = anchor;
        this._preeditVisible = visible;
        this._preeditCommitMode = mode;
    }

    _onShowPreeditText() {
        this._preeditVisible = true;
        this.set_preedit_text(
            this._preeditStr, this._preeditPos, this._preeditAnchor,
            this._preeditCommitMode);
    }

    _onHidePreeditText() {
        this.set_preedit_text(
            null, this._preeditPos, this._preeditAnchor,
            this._preeditCommitMode);
        this._preeditVisible = false;
    }

    _onForwardKeyEvent(_context, keyval, keycode, state) {
        let press = (state & IBus.ModifierType.RELEASE_MASK) === 0;
        state &= ~IBus.ModifierType.RELEASE_MASK;

        let curEvent = Clutter.get_current_event();
        let time;
        if (curEvent)
            time = curEvent.get_time();
        else
            time = global.display.get_current_time_roundtrip();

        this.forward_key(keyval, keycode + 8, state & Clutter.ModifierType.MODIFIER_MASK, time, press);
    }

    vfunc_focus_in(focus) {
        this._currentFocus = focus;
        if (this._context) {
            this.update();
            this._context.focus_in();
            this._emitRequestSurrounding();
        }

        if (this._hidePanelId) {
            GLib.source_remove(this._hidePanelId);
            this._hidePanelId = 0;
        }
    }

    vfunc_focus_out() {
        this._currentFocus = null;
        if (this._context) {
            this._fullReset();
            this._context.focus_out();
        }

        if (this._preeditStr && this._preeditVisible) {
            // Unset any preedit text
            this.set_preedit_text(null, 0, 0, this._preeditCommitMode);
            this._preeditStr = null;
        }

        this._hidePanelId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, HIDE_PANEL_TIME, () => {
            this.set_input_panel_state(Clutter.InputPanelState.OFF);
            this._hidePanelId = 0;
            return GLib.SOURCE_REMOVE;
        });
    }

    vfunc_reset() {
        if (this._context) {
            this._context.reset();
            this._emitRequestSurrounding();
        }

        this._surroundingText = null;
        this._surroundingTextCursor = null;
        this._surroundingTextAnchor = null;
        this._preeditStr = null;
    }

    vfunc_set_cursor_location(rect) {
        if (this._context) {
            this._cursorRect = {
                x: rect.get_x(), y: rect.get_y(),
                width: rect.get_width(), height: rect.get_height(),
            };
            this._context.set_cursor_location(
                this._cursorRect.x, this._cursorRect.y,
                this._cursorRect.width, this._cursorRect.height);
            this._emitRequestSurrounding();
        }
    }

    vfunc_set_surrounding(text, cursor, anchor) {
        this._surroundingText = text;
        this._surroundingTextCursor = cursor;
        this._surroundingTextAnchor = anchor;
        this.emit('surrounding-text-set');

        if (!this._context || (!text && text !== ''))
            return;

        let ibusText = IBus.Text.new_from_string(text);
        this._context.set_surrounding_text(ibusText, cursor, anchor);
    }

    vfunc_update_content_hints(hints) {
        let ibusHints = 0;
        if (hints & Clutter.InputContentHintFlags.COMPLETION)
            ibusHints |= IBus.InputHints.WORD_COMPLETION;
        if (hints & Clutter.InputContentHintFlags.SPELLCHECK)
            ibusHints |= IBus.InputHints.SPELLCHECK;
        if (hints & Clutter.InputContentHintFlags.AUTO_CAPITALIZATION)
            ibusHints |= IBus.InputHints.UPPERCASE_SENTENCES;
        if (hints & Clutter.InputContentHintFlags.LOWERCASE)
            ibusHints |= IBus.InputHints.LOWERCASE;
        if (hints & Clutter.InputContentHintFlags.UPPERCASE)
            ibusHints |= IBus.InputHints.UPPERCASE_CHARS;
        if (hints & Clutter.InputContentHintFlags.TITLECASE)
            ibusHints |= IBus.InputHints.UPPERCASE_WORDS;
        if (hints & Clutter.InputContentHintFlags.SENSITIVE_DATA)
            ibusHints |= IBus.InputHints.PRIVATE;

        this._hints = ibusHints;
        if (this._context)
            this._context.set_content_type(this._purpose, this._hints);
    }

    vfunc_update_content_purpose(purpose) {
        let ibusPurpose = 0;
        if (purpose === Clutter.InputContentPurpose.NORMAL)
            ibusPurpose = IBus.InputPurpose.FREE_FORM;
        else if (purpose === Clutter.InputContentPurpose.ALPHA)
            ibusPurpose = IBus.InputPurpose.ALPHA;
        else if (purpose === Clutter.InputContentPurpose.DIGITS)
            ibusPurpose = IBus.InputPurpose.DIGITS;
        else if (purpose === Clutter.InputContentPurpose.NUMBER)
            ibusPurpose = IBus.InputPurpose.NUMBER;
        else if (purpose === Clutter.InputContentPurpose.PHONE)
            ibusPurpose = IBus.InputPurpose.PHONE;
        else if (purpose === Clutter.InputContentPurpose.URL)
            ibusPurpose = IBus.InputPurpose.URL;
        else if (purpose === Clutter.InputContentPurpose.EMAIL)
            ibusPurpose = IBus.InputPurpose.EMAIL;
        else if (purpose === Clutter.InputContentPurpose.NAME)
            ibusPurpose = IBus.InputPurpose.NAME;
        else if (purpose === Clutter.InputContentPurpose.PASSWORD)
            ibusPurpose = IBus.InputPurpose.PASSWORD;
        else if (purpose === Clutter.InputContentPurpose.TERMINAL &&
                 IBus.InputPurpose.TERMINAL)
            ibusPurpose = IBus.InputPurpose.TERMINAL;

        this._purpose = ibusPurpose;
        if (this._context)
            this._context.set_content_type(this._purpose, this._hints);
    }

    vfunc_filter_key_event(event) {
        if (!this._context)
            return false;
        if (!this._currentSource)
            return false;

        let state = event.get_state();
        if (state & IBus.ModifierType.IGNORED_MASK)
            return false;

        if (event.type() === Clutter.EventType.KEY_RELEASE)
            state |= IBus.ModifierType.RELEASE_MASK;

        this._context.process_key_event_async(
            event.get_key_symbol(),
            event.get_key_code() - 8, // Convert XKB keycodes to evcodes
            state, -1, this._cancellable,
            (context, res) => {
                if (context !== this._context)
                    return;

                try {
                    let retval = context.process_key_event_async_finish(res);
                    this.notify_key_event(event, retval);
                } catch (e) {
                    if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
                        log(`Error processing key on IM: ${e.message}`);
                }
            });
        return true;
    }

    getSurroundingText() {
        return [
            this._surroundingText,
            this._surroundingTextCursor,
            this._surroundingTextAnchor,
        ];
    }

    hasPreedit() {
        return this._preeditVisible && this._preeditStr !== '' && this._preeditStr !== null;
    }

    async handleVirtualKey(keyval) {
        try {
            if (!await this._context.process_key_event_async(
                keyval, 0, 0, -1, null))
                return false;

            await this._context.process_key_event_async(
                keyval, 0, IBus.ModifierType.RELEASE_MASK, -1, null);
            return true;
        } catch (e) {
            return false;
        }
    }

    _fullReset() {
        this._context.set_content_type(0, 0);
        this._context.set_cursor_location(0, 0, 0, 0);
        this._context.reset();
    }

    update() {
        if (!this._context)
            return;
        this._updateCapabilities();
        this._context.set_content_type(this._purpose, this._hints);
        if (this._cursorRect) {
            this._context.set_cursor_location(
                this._cursorRect.x, this._cursorRect.y,
                this._cursorRect.width, this._cursorRect.height);
        }
        this._emitRequestSurrounding();
    }
});
(uuay)workspacesView.jsW�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Mtk from 'gi://Mtk';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Layout from './layout.js';

import * as Main from './main.js';
import * as OverviewControls from './overviewControls.js';
import * as SwipeTracker from './swipeTracker.js';
import * as Util from '../misc/util.js';
import * as Workspace from './workspace.js';
import {ThumbnailsBox} from './workspaceThumbnail.js';

const WORKSPACE_SWITCH_TIME = 250;

const MUTTER_SCHEMA = 'org.gnome.mutter';

const WORKSPACE_MIN_SPACING = 24;
const WORKSPACE_MAX_SPACING = 80;

const WORKSPACE_INACTIVE_SCALE = 0.94;

const SECONDARY_WORKSPACE_SCALE = 0.80;

const WorkspacesViewBase = GObject.registerClass({
    GTypeFlags: GObject.TypeFlags.ABSTRACT,
}, class WorkspacesViewBase extends St.Widget {
    _init(monitorIndex, overviewAdjustment) {
        super._init({
            style_class: 'workspaces-view',
            x_expand: true,
            y_expand: true,
        });
        this.connect('destroy', this._onDestroy.bind(this));
        global.focus_manager.add_group(this);

        this._monitorIndex = monitorIndex;

        this._inDrag = false;
        Main.overview.connectObject(
            'window-drag-begin', this._dragBegin.bind(this),
            'window-drag-end', this._dragEnd.bind(this), this);

        this._overviewAdjustment = overviewAdjustment;
        overviewAdjustment.connectObject('notify::value',
            () => this._updateWorkspaceMode(), this);
    }

    _onDestroy() {
        this._dragEnd();
    }

    _dragBegin() {
        this._inDrag = true;
    }

    _dragEnd() {
        this._inDrag = false;
    }

    _updateWorkspaceMode() {
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        for (const child of this)
            child.allocate_available_size(0, 0, box.get_width(), box.get_height());
    }

    vfunc_get_preferred_width() {
        return [0, 0];
    }

    vfunc_get_preferred_height() {
        return [0, 0];
    }
});

/** @enum {number} */
export const FitMode = {
    SINGLE: 0,
    ALL: 1,
};

export const WorkspacesView = GObject.registerClass(
class WorkspacesView extends WorkspacesViewBase {
    _init(monitorIndex, controls, scrollAdjustment, fitModeAdjustment, overviewAdjustment) {
        let workspaceManager = global.workspace_manager;

        super._init(monitorIndex, overviewAdjustment);

        this._controls = controls;
        this._fitModeAdjustment = fitModeAdjustment;
        this._fitModeAdjustment.connectObject('notify::value', () => {
            this._updateVisibility();
            this._updateWorkspacesState();
            this.queue_relayout();
        }, this);

        this._animating = false; // tweening
        this._gestureActive = false; // touch(pad) gestures

        this._scrollAdjustment = scrollAdjustment;
        this._scrollAdjustment.connectObject('notify::value',
            this._onScrollAdjustmentChanged.bind(this), this);

        this._workspaces = [];
        this._updateWorkspaces();
        workspaceManager.connectObject(
            'notify::n-workspaces', this._updateWorkspaces.bind(this),
            'workspaces-reordered', () => {
                this._workspaces.sort((a, b) => {
                    return a.metaWorkspace.index() - b.metaWorkspace.index();
                });
                this._workspaces.forEach(
                    (ws, i) => this.set_child_at_index(ws, i));
            }, this);

        global.window_manager.connectObject('switch-workspace',
            this._activeWorkspaceChanged.bind(this), this);
    }

    _getFirstFitAllWorkspaceBox(box, spacing, vertical) {
        const {nWorkspaces} = global.workspaceManager;
        const [width, height] = box.get_size();
        const [workspace] = this._workspaces;

        const fitAllBox = new Clutter.ActorBox();

        let [x1, y1] = box.get_origin();

        // Spacing here is not only the space between workspaces, but also the
        // space before the first workspace, and after the last one. This prevents
        // workspaces from touching the edges of the allocation box.
        if (vertical) {
            const availableHeight = height - spacing * (nWorkspaces + 1);
            let workspaceHeight = availableHeight / nWorkspaces;
            let [, workspaceWidth] =
                workspace.get_preferred_width(workspaceHeight);

            y1 = spacing;
            if (workspaceWidth > width) {
                [, workspaceHeight] = workspace.get_preferred_height(width);
                y1 += Math.max((availableHeight - workspaceHeight * nWorkspaces) / 2, 0);
            }

            fitAllBox.set_size(width, workspaceHeight);
        } else {
            const availableWidth = width - spacing * (nWorkspaces + 1);
            let workspaceWidth = availableWidth / nWorkspaces;
            let [, workspaceHeight] =
                workspace.get_preferred_height(workspaceWidth);

            x1 = spacing;
            if (workspaceHeight > height) {
                [, workspaceWidth] = workspace.get_preferred_width(height);
                x1 += Math.max((availableWidth - workspaceWidth * nWorkspaces) / 2, 0);
            }

            fitAllBox.set_size(workspaceWidth, height);
        }

        fitAllBox.set_origin(x1, y1);

        return fitAllBox;
    }

    _getFirstFitSingleWorkspaceBox(box, spacing, vertical) {
        const [width, height] = box.get_size();
        const [workspace] = this._workspaces;

        const rtl = this.text_direction === Clutter.TextDirection.RTL;
        const adj = this._scrollAdjustment;
        const currentWorkspace = vertical || !rtl
            ? adj.value : adj.upper - adj.value - 1;

        // Single fit mode implies centered too
        let [x1, y1] = box.get_origin();
        if (vertical) {
            const [, workspaceHeight] = workspace.get_preferred_height(width);
            y1 += (height - workspaceHeight) / 2;
            y1 -= currentWorkspace * (workspaceHeight + spacing);
        } else {
            const [, workspaceWidth] = workspace.get_preferred_width(height);
            x1 += (width - workspaceWidth) / 2;
            x1 -= currentWorkspace * (workspaceWidth + spacing);
        }

        const fitSingleBox = new Clutter.ActorBox({x1, y1});

        if (vertical) {
            const [, workspaceHeight] = workspace.get_preferred_height(width);
            fitSingleBox.set_size(width, workspaceHeight);
        } else {
            const [, workspaceWidth] = workspace.get_preferred_width(height);
            fitSingleBox.set_size(workspaceWidth, height);
        }

        return fitSingleBox;
    }

    _getSpacing(box, fitMode, vertical) {
        const [width, height] = box.get_size();
        const [workspace] = this._workspaces;

        let availableSpace;
        let workspaceSize;
        if (vertical) {
            [, workspaceSize] = workspace.get_preferred_height(width);
            availableSpace = (height - workspaceSize) / 2;
        } else {
            [, workspaceSize] = workspace.get_preferred_width(height);
            availableSpace = (width - workspaceSize) / 2;
        }

        const spacing = (availableSpace - workspaceSize * 0.4) * (1 - fitMode);
        const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);

        return Math.clamp(spacing, WORKSPACE_MIN_SPACING * scaleFactor,
            WORKSPACE_MAX_SPACING * scaleFactor);
    }

    _getWorkspaceModeForOverviewState(state) {
        const {ControlsState} = OverviewControls;

        switch (state) {
        case ControlsState.HIDDEN:
            return 0;
        case ControlsState.WINDOW_PICKER:
            return 1;
        case ControlsState.APP_GRID:
            return 0;
        }

        return 0;
    }

    _updateWorkspacesState() {
        const adj = this._scrollAdjustment;
        const fitMode = this._fitModeAdjustment.value;

        const {initialState, finalState, progress} =
            this._overviewAdjustment.getStateTransitionParams();

        const workspaceMode = (1 - fitMode) * Util.lerp(
            this._getWorkspaceModeForOverviewState(initialState),
            this._getWorkspaceModeForOverviewState(finalState),
            progress);

        // Fade and scale inactive workspaces
        this._workspaces.forEach((w, index) => {
            w.stateAdjustment.value = workspaceMode;

            const distanceToCurrentWorkspace = Math.abs(adj.value - index);

            const scaleProgress = 1 - Math.clamp(distanceToCurrentWorkspace, 0, 1);

            const scale = Util.lerp(WORKSPACE_INACTIVE_SCALE, 1, scaleProgress);
            w.set_scale(scale, scale);
        });
    }

    _getFitModeForState(state) {
        const {ControlsState} = OverviewControls;

        switch (state) {
        case ControlsState.HIDDEN:
        case ControlsState.WINDOW_PICKER:
            return FitMode.SINGLE;
        case ControlsState.APP_GRID:
            return FitMode.ALL;
        default:
            return FitMode.SINGLE;
        }
    }

    _getInitialBoxes(box) {
        const offsetBox = new Clutter.ActorBox();
        offsetBox.set_size(...box.get_size());

        let fitSingleBox = offsetBox;
        let fitAllBox = offsetBox;

        const {transitioning, initialState, finalState} =
            this._overviewAdjustment.getStateTransitionParams();

        const isPrimary = Main.layoutManager.primaryIndex === this._monitorIndex;

        if (isPrimary && transitioning) {
            const initialFitMode = this._getFitModeForState(initialState);
            const finalFitMode = this._getFitModeForState(finalState);

            // Only use the relative boxes when the overview is in a state
            // transition, and the corresponding fit modes are different.
            if (initialFitMode !== finalFitMode) {
                const initialBox =
                    this._controls.getWorkspacesBoxForState(initialState).copy();
                const finalBox =
                    this._controls.getWorkspacesBoxForState(finalState).copy();

                // Boxes are relative to ControlsManager, transform them;
                //   this.apply_relative_transform_to_point(controls,
                //       new Graphene.Point3D());
                // would be more correct, but also more expensive
                const [parentOffsetX, parentOffsetY] =
                    this.get_parent().allocation.get_origin();
                [initialBox, finalBox].forEach(b => {
                    b.set_origin(b.x1 - parentOffsetX, b.y1 - parentOffsetY);
                });

                if (initialFitMode === FitMode.SINGLE)
                    [fitSingleBox, fitAllBox] = [initialBox, finalBox];
                else
                    [fitAllBox, fitSingleBox] = [initialBox, finalBox];
            }
        }

        return [fitSingleBox, fitAllBox];
    }

    _updateWorkspaceMode() {
        this._updateWorkspacesState();
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        if (this._workspaces.length === 0)
            return;

        const vertical = global.workspaceManager.layout_rows === -1;
        const rtl = this.text_direction === Clutter.TextDirection.RTL;

        const fitMode = this._fitModeAdjustment.value;

        let [fitSingleBox, fitAllBox] = this._getInitialBoxes(box);
        const fitSingleSpacing =
            this._getSpacing(fitSingleBox, FitMode.SINGLE, vertical);
        fitSingleBox =
            this._getFirstFitSingleWorkspaceBox(fitSingleBox, fitSingleSpacing, vertical);

        const fitAllSpacing =
            this._getSpacing(fitAllBox, FitMode.ALL, vertical);
        fitAllBox =
            this._getFirstFitAllWorkspaceBox(fitAllBox, fitAllSpacing, vertical);

        // Account for RTL locales by reversing the list
        const workspaces = this._workspaces.slice();
        if (rtl)
            workspaces.reverse();

        const [fitSingleX1, fitSingleY1] = fitSingleBox.get_origin();
        const [fitSingleWidth, fitSingleHeight] = fitSingleBox.get_size();
        const [fitAllX1, fitAllY1] = fitAllBox.get_origin();
        const [fitAllWidth, fitAllHeight] = fitAllBox.get_size();

        workspaces.forEach(child => {
            if (fitMode === FitMode.SINGLE)
                box = fitSingleBox;
            else if (fitMode === FitMode.ALL)
                box = fitAllBox;
            else
                box = fitSingleBox.interpolate(fitAllBox, fitMode);

            child.allocate_align_fill(box, 0.5, 0.5, false, false);

            if (vertical) {
                fitSingleBox.set_origin(
                    fitSingleX1,
                    fitSingleBox.y1 + fitSingleHeight + fitSingleSpacing);
                fitAllBox.set_origin(
                    fitAllX1,
                    fitAllBox.y1 + fitAllHeight + fitAllSpacing);
            } else {
                fitSingleBox.set_origin(
                    fitSingleBox.x1 + fitSingleWidth + fitSingleSpacing,
                    fitSingleY1);
                fitAllBox.set_origin(
                    fitAllBox.x1 + fitAllWidth + fitAllSpacing,
                    fitAllY1);
            }
        });
    }

    getActiveWorkspace() {
        let workspaceManager = global.workspace_manager;
        let active = workspaceManager.get_active_workspace_index();
        return this._workspaces[active];
    }

    prepareToLeaveOverview() {
        for (let w = 0; w < this._workspaces.length; w++)
            this._workspaces[w].prepareToLeaveOverview();
    }

    syncStacking(stackIndices) {
        for (let i = 0; i < this._workspaces.length; i++)
            this._workspaces[i].syncStacking(stackIndices);
    }

    _scrollToActive() {
        const {workspaceManager} = global;
        const active = workspaceManager.get_active_workspace_index();

        this._animating = true;
        this._updateVisibility();

        this._scrollAdjustment.remove_transition('value');
        this._scrollAdjustment.ease(active, {
            duration: WORKSPACE_SWITCH_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            onComplete: () => {
                this._animating = false;
                this._updateVisibility();
            },
        });
    }

    _updateVisibility() {
        let workspaceManager = global.workspace_manager;
        let active = workspaceManager.get_active_workspace_index();

        const fitMode = this._fitModeAdjustment.value;
        const singleFitMode = fitMode === FitMode.SINGLE;

        for (let w = 0; w < this._workspaces.length; w++) {
            let workspace = this._workspaces[w];

            if (this._animating || this._gestureActive || !singleFitMode)
                workspace.show();
            else
                workspace.visible = Math.abs(w - active) <= 1;
        }
    }

    _updateWorkspaces() {
        let workspaceManager = global.workspace_manager;
        let newNumWorkspaces = workspaceManager.n_workspaces;

        for (let j = 0; j < newNumWorkspaces; j++) {
            let metaWorkspace = workspaceManager.get_workspace_by_index(j);
            let workspace;

            if (j >= this._workspaces.length) { /* added */
                workspace = new Workspace.Workspace(
                    metaWorkspace,
                    this._monitorIndex,
                    this._overviewAdjustment);
                this.add_child(workspace);
                this._workspaces[j] = workspace;
            } else  {
                workspace = this._workspaces[j];

                if (workspace.metaWorkspace !== metaWorkspace) { /* removed */
                    workspace.destroy();
                    this._workspaces.splice(j, 1);
                } /* else kept */
            }
        }

        for (let j = this._workspaces.length - 1; j >= newNumWorkspaces; j--) {
            this._workspaces[j].destroy();
            this._workspaces.splice(j, 1);
        }

        this._updateWorkspacesState();
        this._updateVisibility();
    }

    _activeWorkspaceChanged(_wm, _from, _to, _direction) {
        this._scrollToActive();
    }

    _onDestroy() {
        super._onDestroy();

        this._workspaces = [];
    }

    startTouchGesture() {
        this._gestureActive = true;

        this._updateVisibility();
    }

    endTouchGesture() {
        this._gestureActive = false;

        // Make sure title captions etc are shown as necessary
        this._scrollToActive();
        this._updateVisibility();
    }

    // sync the workspaces' positions to the value of the scroll adjustment
    // and change the active workspace if appropriate
    _onScrollAdjustmentChanged() {
        if (!this.has_allocation())
            return;

        const adj = this._scrollAdjustment;
        const allowSwitch =
            adj.get_transition('value') === null && !this._gestureActive;

        let workspaceManager = global.workspace_manager;
        let active = workspaceManager.get_active_workspace_index();
        let current = Math.round(adj.value);

        if (allowSwitch && active !== current) {
            if (!this._workspaces[current]) {
                // The current workspace was destroyed. This could happen
                // when you are on the last empty workspace, and consolidate
                // windows using the thumbnail bar.
                // In that case, the intended behavior is to stay on the empty
                // workspace, which is the last one, so pick it.
                current = this._workspaces.length - 1;
            }

            let metaWorkspace = this._workspaces[current].metaWorkspace;
            metaWorkspace.activate(global.get_current_time());
        }

        this._updateWorkspacesState();
        this.queue_relayout();
    }
});

export const ExtraWorkspaceView = GObject.registerClass(
class ExtraWorkspaceView extends WorkspacesViewBase {
    _init(monitorIndex, overviewAdjustment) {
        super._init(monitorIndex, overviewAdjustment);
        this._workspace =
            new Workspace.Workspace(null, monitorIndex, overviewAdjustment);
        this.add_child(this._workspace);
    }

    _updateWorkspaceMode() {
        const overviewState = this._overviewAdjustment.value;

        const progress = Math.clamp(overviewState,
            OverviewControls.ControlsState.HIDDEN,
            OverviewControls.ControlsState.WINDOW_PICKER);

        this._workspace.stateAdjustment.value = progress;
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        const [width, height] = box.get_size();
        const [, childWidth] = this._workspace.get_preferred_width(height);

        const childBox = new Clutter.ActorBox();
        childBox.set_origin(Math.round((width - childWidth) / 2), 0);
        childBox.set_size(childWidth, height);
        this._workspace.allocate(childBox);
    }

    getActiveWorkspace() {
        return this._workspace;
    }

    prepareToLeaveOverview() {
        this._workspace.prepareToLeaveOverview();
    }

    syncStacking(stackIndices) {
        this._workspace.syncStacking(stackIndices);
    }

    startTouchGesture() {
    }

    endTouchGesture() {
    }
});

export const SecondaryMonitorDisplay = GObject.registerClass(
class SecondaryMonitorDisplay extends St.Widget {
    _init(monitorIndex, controls, scrollAdjustment, fitModeAdjustment, overviewAdjustment) {
        this._monitorIndex = monitorIndex;
        this._controls = controls;
        this._scrollAdjustment = scrollAdjustment;
        this._fitModeAdjustment = fitModeAdjustment;
        this._overviewAdjustment = overviewAdjustment;

        super._init({
            style_class: 'secondary-monitor-workspaces',
            constraints: new Layout.MonitorConstraint({
                index: this._monitorIndex,
                work_area: true,
            }),
            clip_to_allocation: true,
        });

        this.connect('destroy', () => this._onDestroy());

        this._thumbnails = new ThumbnailsBox(
            this._scrollAdjustment, monitorIndex);
        this.add_child(this._thumbnails);

        this._thumbnails.connect('notify::should-show',
            () => this._updateThumbnailVisibility());

        this._overviewAdjustment.connectObject('notify::value', () => {
            this._updateThumbnailParams();
            this.queue_relayout();
        }, this);

        this._settings = new Gio.Settings({schema_id: MUTTER_SCHEMA});
        this._settings.connect('changed::workspaces-only-on-primary',
            () => this._workspacesOnPrimaryChanged());
        this._workspacesOnPrimaryChanged();
    }

    _getThumbnailParamsForState(state) {
        const {ControlsState} = OverviewControls;

        let opacity, scale;
        switch (state) {
        case ControlsState.HIDDEN:
        case ControlsState.WINDOW_PICKER:
            opacity = 255;
            scale = 1;
            break;
        case ControlsState.APP_GRID:
            opacity = 0;
            scale = 0.5;
            break;
        default:
            opacity = 255;
            scale = 1;
            break;
        }

        return {opacity, scale};
    }

    _getThumbnailsHeight(box) {
        if (!this._thumbnails.visible)
            return 0;

        const [width, height] = box.get_size();
        const {expandFraction} = this._thumbnails;
        const [thumbnailsHeight] = this._thumbnails.get_preferred_height(width);
        return Math.min(
            thumbnailsHeight * expandFraction,
            height * this._thumbnails.maxThumbnailScale);
    }

    _getWorkspacesBoxForState(state, box, padding, thumbnailsHeight, spacing) {
        const {ControlsState} = OverviewControls;
        const workspaceBox = box.copy();
        const [width, height] = workspaceBox.get_size();

        switch (state) {
        case ControlsState.HIDDEN:
            break;
        case ControlsState.WINDOW_PICKER:
            workspaceBox.set_origin(0, padding + thumbnailsHeight + spacing);
            workspaceBox.set_size(
                width,
                height - 2 * padding - thumbnailsHeight - spacing);
            break;
        case ControlsState.APP_GRID:
            workspaceBox.set_origin(0, padding);
            workspaceBox.set_size(
                width,
                height - 2 * padding);
            break;
        }

        return workspaceBox;
    }

    vfunc_allocate(box) {
        this.set_allocation(box);

        const themeNode = this.get_theme_node();
        const contentBox = themeNode.get_content_box(box);
        const [width, height] = contentBox.get_size();
        const {expandFraction} = this._thumbnails;
        const spacing = themeNode.get_length('spacing') * expandFraction;
        const padding =
            Math.round((1 - SECONDARY_WORKSPACE_SCALE) * height / 2);

        const thumbnailsHeight = this._getThumbnailsHeight(contentBox);

        if (this._thumbnails.visible) {
            const childBox = new Clutter.ActorBox();
            childBox.set_origin(0, padding);
            childBox.set_size(width, thumbnailsHeight);
            this._thumbnails.allocate(childBox);
        }

        const {
            currentState, initialState, finalState, transitioning, progress,
        } = this._overviewAdjustment.getStateTransitionParams();

        let workspacesBox;
        const workspaceParams = [contentBox, padding, thumbnailsHeight, spacing];
        if (!transitioning) {
            workspacesBox =
                this._getWorkspacesBoxForState(currentState, ...workspaceParams);
        } else {
            const initialBox =
                this._getWorkspacesBoxForState(initialState, ...workspaceParams);
            const finalBox =
                this._getWorkspacesBoxForState(finalState, ...workspaceParams);
            workspacesBox = initialBox.interpolate(finalBox, progress);
        }
        this._workspacesView.allocate(workspacesBox);
    }

    _onDestroy() {
        if (this._settings)
            this._settings.run_dispose();
        this._settings = null;
    }

    _workspacesOnPrimaryChanged() {
        this._updateWorkspacesView();
        this._updateThumbnailVisibility();
    }

    _updateWorkspacesView() {
        if (this._workspacesView)
            this._workspacesView.destroy();

        if (this._settings.get_boolean('workspaces-only-on-primary')) {
            this._workspacesView = new ExtraWorkspaceView(
                this._monitorIndex,
                this._overviewAdjustment);
        } else {
            this._workspacesView = new WorkspacesView(
                this._monitorIndex,
                this._controls,
                this._scrollAdjustment,
                this._fitModeAdjustment,
                this._overviewAdjustment);
        }
        this.add_child(this._workspacesView);
    }

    _updateThumbnailVisibility() {
        const visible =
            this._thumbnails.should_show &&
            !this._settings.get_boolean('workspaces-only-on-primary');

        if (this._thumbnails.visible === visible)
            return;

        this._thumbnails.show();
        this._updateThumbnailParams();
        this._thumbnails.ease_property('expand-fraction', visible ? 1 : 0, {
            duration: OverviewControls.SIDE_CONTROLS_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => (this._thumbnails.visible = visible),
        });
    }

    _updateThumbnailParams() {
        if (!this._thumbnails.visible)
            return;

        const {initialState, finalState, progress} =
            this._overviewAdjustment.getStateTransitionParams();

        const initialParams = this._getThumbnailParamsForState(initialState);
        const finalParams = this._getThumbnailParamsForState(finalState);

        const opacity =
            Util.lerp(initialParams.opacity, finalParams.opacity, progress);
        const scale =
            Util.lerp(initialParams.scale, finalParams.scale, progress);

        this._thumbnails.set({
            opacity,
            scale_x: scale,
            scale_y: scale,
        });
    }

    getActiveWorkspace() {
        return this._workspacesView.getActiveWorkspace();
    }

    prepareToLeaveOverview() {
        this._workspacesView.prepareToLeaveOverview();
    }

    syncStacking(stackIndices) {
        this._workspacesView.syncStacking(stackIndices);
    }

    startTouchGesture() {
        this._workspacesView.startTouchGesture();
    }

    endTouchGesture() {
        this._workspacesView.endTouchGesture();
    }
});

export const WorkspacesDisplay = GObject.registerClass(
class WorkspacesDisplay extends St.Widget {
    _init(controls, scrollAdjustment, overviewAdjustment) {
        super._init({
            layout_manager: new Clutter.BinLayout(),
            reactive: true,
        });

        this._controls = controls;
        this._overviewAdjustment = overviewAdjustment;
        this._fitModeAdjustment = new St.Adjustment({
            actor: this,
            value: FitMode.SINGLE,
            lower: FitMode.SINGLE,
            upper: FitMode.ALL,
        });

        let workspaceManager = global.workspace_manager;
        this._scrollAdjustment = scrollAdjustment;

        global.window_manager.connectObject('switch-workspace',
            this._activeWorkspaceChanged.bind(this), this);

        this._swipeTracker = new SwipeTracker.SwipeTracker(
            Main.layoutManager.overviewGroup,
            Clutter.Orientation.HORIZONTAL,
            Shell.ActionMode.OVERVIEW,
            {allowDrag: false});
        this._swipeTracker.allowLongSwipes = true;
        this._swipeTracker.connect('begin', this._switchWorkspaceBegin.bind(this));
        this._swipeTracker.connect('update', this._switchWorkspaceUpdate.bind(this));
        this._swipeTracker.connect('end', this._switchWorkspaceEnd.bind(this));
        this.connect('notify::mapped', this._updateSwipeTracker.bind(this));

        workspaceManager.connectObject(
            'workspaces-reordered', this._workspacesReordered.bind(this),
            'notify::layout-rows', this._updateTrackerOrientation.bind(this), this);
        this._updateTrackerOrientation();

        Main.overview.connectObject(
            'window-drag-begin', this._windowDragBegin.bind(this),
            'window-drag-end', this._windowDragEnd.bind(this), this);

        this._primaryVisible = true;
        this._primaryIndex = Main.layoutManager.primaryIndex;
        this._workspacesViews = [];

        this._settings = new Gio.Settings({schema_id: MUTTER_SCHEMA});

        this._inWindowDrag = false;
        this._leavingOverview = false;

        this._gestureActive = false; // touch(pad) gestures
    }

    _windowDragBegin() {
        this._inWindowDrag = true;
        this._updateSwipeTracker();
    }

    _windowDragEnd() {
        this._inWindowDrag = false;
        this._updateSwipeTracker();
    }

    _updateSwipeTracker() {
        this._swipeTracker.enabled =
            this.mapped &&
            !this._inWindowDrag &&
            !this._leavingOverview;
    }

    _workspacesReordered() {
        let workspaceManager = global.workspace_manager;

        this._scrollAdjustment.value =
            workspaceManager.get_active_workspace_index();
    }

    _activeWorkspaceChanged(_wm, _from, to, _direction) {
        if (this._gestureActive)
            return;

        this._scrollAdjustment.ease(to, {
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            duration: WORKSPACE_SWITCH_TIME,
        });
    }

    _updateTrackerOrientation() {
        const {layoutRows} = global.workspace_manager;
        this._swipeTracker.orientation = layoutRows !== -1
            ? Clutter.Orientation.HORIZONTAL
            : Clutter.Orientation.VERTICAL;
    }

    _directionForProgress(progress) {
        if (global.workspace_manager.layout_rows === -1) {
            return progress > 0
                ? Meta.MotionDirection.DOWN
                : Meta.MotionDirection.UP;
        } else if (this.text_direction === Clutter.TextDirection.RTL) {
            return progress > 0
                ? Meta.MotionDirection.LEFT
                : Meta.MotionDirection.RIGHT;
        } else {
            return progress > 0
                ? Meta.MotionDirection.RIGHT
                : Meta.MotionDirection.LEFT;
        }
    }

    _switchWorkspaceBegin(tracker, monitor) {
        if (this._workspacesOnlyOnPrimary && monitor !== this._primaryIndex)
            return;

        let workspaceManager = global.workspace_manager;
        let adjustment = this._scrollAdjustment;
        if (this._gestureActive)
            adjustment.remove_transition('value');

        const distance = global.workspace_manager.layout_rows === -1
            ? this.height : this.width;

        for (let i = 0; i < this._workspacesViews.length; i++)
            this._workspacesViews[i].startTouchGesture();

        let progress = adjustment.value / adjustment.page_size;
        let points = Array.from(
            {length: workspaceManager.n_workspaces}, (v, i) => i);

        tracker.confirmSwipe(distance, points, progress, Math.round(progress));

        this._gestureActive = true;
    }

    _switchWorkspaceUpdate(tracker, progress) {
        let adjustment = this._scrollAdjustment;
        adjustment.value = progress * adjustment.page_size;
    }

    _switchWorkspaceEnd(tracker, duration, endProgress) {
        let workspaceManager = global.workspace_manager;
        let newWs = workspaceManager.get_workspace_by_index(endProgress);

        this._scrollAdjustment.ease(endProgress, {
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            duration,
            onComplete: () => {
                if (!newWs.active)
                    newWs.activate(global.get_current_time());
                this._endTouchGesture();
            },
        });
    }

    _endTouchGesture() {
        for (let i = 0; i < this._workspacesViews.length; i++)
            this._workspacesViews[i].endTouchGesture();
        this._gestureActive = false;
    }

    vfunc_navigate_focus(from, direction) {
        return this._getPrimaryView()?.navigate_focus(from, direction, false);
    }

    setPrimaryWorkspaceVisible(visible) {
        if (this._primaryVisible === visible)
            return;

        this._primaryVisible = visible;

        const primaryIndex = Main.layoutManager.primaryIndex;
        const primaryWorkspace = this._workspacesViews[primaryIndex];
        if (primaryWorkspace)
            primaryWorkspace.visible = visible;
    }

    prepareToEnterOverview() {
        this.show();
        this._updateWorkspacesViews();

        Main.overview.connectObject(
            'windows-restacked', this._onRestacked.bind(this),
            'scroll-event', this._onScrollEvent.bind(this), this);

        global.stage.connectObject(
            'key-press-event', this._onKeyPressEvent.bind(this), this);
    }

    prepareToLeaveOverview() {
        for (let i = 0; i < this._workspacesViews.length; i++)
            this._workspacesViews[i].prepareToLeaveOverview();

        this._leavingOverview = true;
        this._updateSwipeTracker();
    }

    vfunc_hide() {
        Main.overview.disconnectObject(this);
        global.stage.disconnectObject(this);

        for (let i = 0; i < this._workspacesViews.length; i++)
            this._workspacesViews[i].destroy();
        this._workspacesViews = [];

        this._leavingOverview = false;

        super.vfunc_hide();
    }

    _updateWorkspacesViews() {
        for (let i = 0; i < this._workspacesViews.length; i++)
            this._workspacesViews[i].destroy();

        this._primaryIndex = Main.layoutManager.primaryIndex;
        this._workspacesViews = [];
        let monitors = Main.layoutManager.monitors;
        for (let i = 0; i < monitors.length; i++) {
            let view;
            if (i === this._primaryIndex) {
                view = new WorkspacesView(i,
                    this._controls,
                    this._scrollAdjustment,
                    this._fitModeAdjustment,
                    this._overviewAdjustment);

                view.visible = this._primaryVisible;
                this.bind_property('opacity', view, 'opacity', GObject.BindingFlags.SYNC_CREATE);
                this.add_child(view);
            } else {
                view = new SecondaryMonitorDisplay(i,
                    this._controls,
                    this._scrollAdjustment,
                    this._fitModeAdjustment,
                    this._overviewAdjustment);
                Main.layoutManager.overviewGroup.add_child(view);
            }

            this._workspacesViews.push(view);
        }
    }

    _getMonitorIndexForEvent(event) {
        let [x, y] = event.get_coords();
        const rect = new Mtk.Rectangle({x, y, width: 1, height: 1});
        return global.display.get_monitor_index_for_rect(rect);
    }

    _getPrimaryView() {
        if (!this._workspacesViews.length)
            return null;
        return this._workspacesViews[this._primaryIndex];
    }

    activeWorkspaceHasMaximizedWindows() {
        const primaryView = this._getPrimaryView();
        return primaryView
            ? primaryView.getActiveWorkspace().hasMaximizedWindows()
            : false;
    }

    _onRestacked(overview, stackIndices) {
        for (let i = 0; i < this._workspacesViews.length; i++)
            this._workspacesViews[i].syncStacking(stackIndices);
    }

    _onScrollEvent(actor, event) {
        if (this._swipeTracker.canHandleScrollEvent(event))
            return Clutter.EVENT_PROPAGATE;

        if (!this.mapped)
            return Clutter.EVENT_PROPAGATE;

        if (this._workspacesOnlyOnPrimary &&
            this._getMonitorIndexForEvent(event) !== this._primaryIndex)
            return Clutter.EVENT_PROPAGATE;

        return Main.wm.handleWorkspaceScroll(event);
    }

    _onKeyPressEvent(actor, event) {
        const {ControlsState} = OverviewControls;
        if (this._overviewAdjustment.value !== ControlsState.WINDOW_PICKER)
            return Clutter.EVENT_PROPAGATE;

        if (!this.reactive)
            return Clutter.EVENT_PROPAGATE;

        const {workspaceManager} = global;
        const vertical = workspaceManager.layout_rows === -1;
        const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;

        let which;
        switch (event.get_key_symbol()) {
        case Clutter.KEY_Page_Up:
            if (vertical)
                which = Meta.MotionDirection.UP;
            else if (rtl)
                which = Meta.MotionDirection.RIGHT;
            else
                which = Meta.MotionDirection.LEFT;
            break;
        case Clutter.KEY_Page_Down:
            if (vertical)
                which = Meta.MotionDirection.DOWN;
            else if (rtl)
                which = Meta.MotionDirection.LEFT;
            else
                which = Meta.MotionDirection.RIGHT;
            break;
        case Clutter.KEY_Home:
            which = 0;
            break;
        case Clutter.KEY_End:
            which = workspaceManager.n_workspaces - 1;
            break;
        default:
            return Clutter.EVENT_PROPAGATE;
        }

        let ws;
        if (which < 0)
            // Negative workspace numbers are directions
            // with respect to the current workspace
            ws = workspaceManager.get_active_workspace().get_neighbor(which);
        else
            // Otherwise it is a workspace index
            ws = workspaceManager.get_workspace_by_index(which);

        if (ws)
            Main.wm.actionMoveWorkspace(ws);

        return Clutter.EVENT_STOP;
    }

    get _workspacesOnlyOnPrimary() {
        return this._settings.get_boolean('workspaces-only-on-primary');
    }

    get fitModeAdjustment() {
        return this._fitModeAdjustment;
    }
});
(uuay)windowManager.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Mtk from 'gi://Mtk';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as AltTab from './altTab.js';
import * as AppFavorites from './appFavorites.js';
import * as Dialog from './dialog.js';
import * as WorkspaceSwitcherPopup from './workspaceSwitcherPopup.js';
import * as InhibitShortcutsDialog from './inhibitShortcutsDialog.js';
import * as ModalDialog from './modalDialog.js';
import * as WindowMenu from './windowMenu.js';
import * as PadOsd from './padOsd.js';
import * as EdgeDragAction from './edgeDragAction.js';
import * as CloseDialog from './closeDialog.js';
import * as SwitchMonitor from './switchMonitor.js';
import * as IBusManager from '../misc/ibusManager.js';
import * as WorkspaceAnimation from './workspaceAnimation.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';
import * as Main from './main.js';

export const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';

const MINIMIZE_WINDOW_ANIMATION_TIME = 400;
const MINIMIZE_WINDOW_ANIMATION_MODE = Clutter.AnimationMode.EASE_OUT_EXPO;
const SHOW_WINDOW_ANIMATION_TIME = 150;
const DIALOG_SHOW_WINDOW_ANIMATION_TIME = 100;
const DESTROY_WINDOW_ANIMATION_TIME = 150;
const DIALOG_DESTROY_WINDOW_ANIMATION_TIME = 100;
const WINDOW_ANIMATION_TIME = 250;
export const SCROLL_TIMEOUT_TIME = 150;
const DIM_BRIGHTNESS = -0.3;
const DIM_TIME = 500;
const UNDIM_TIME = 250;

const ONE_SECOND = 1000; // in ms

const MIN_NUM_WORKSPACES = 2;

const GSD_WACOM_BUS_NAME = 'org.gnome.SettingsDaemon.Wacom';
const GSD_WACOM_OBJECT_PATH = '/org/gnome/SettingsDaemon/Wacom';

const GsdWacomIface = loadInterfaceXML('org.gnome.SettingsDaemon.Wacom');
const GsdWacomProxy = Gio.DBusProxy.makeProxyWrapper(GsdWacomIface);

const WINDOW_DIMMER_EFFECT_NAME = 'gnome-shell-window-dimmer';

Gio._promisify(Shell, 'util_start_systemd_unit');
Gio._promisify(Shell, 'util_stop_systemd_unit');

const DisplayChangeDialog = GObject.registerClass(
class DisplayChangeDialog extends ModalDialog.ModalDialog {
    _init(wm) {
        super._init();

        this._wm = wm;

        const monitorManager = global.backend.get_monitor_manager();
        this._countDown = monitorManager.get_display_configuration_timeout();

        // Translators: This string should be shorter than 30 characters
        let title = _('Keep these display settings?');
        let description = this._formatCountDown();

        this._content = new Dialog.MessageDialogContent({title, description});
        this.contentLayout.add_child(this._content);

        /* Translators: this and the following message should be limited in length,
           to avoid ellipsizing the labels.
        */
        this._cancelButton = this.addButton({
            label: _('Revert Settings'),
            action: this._onFailure.bind(this),
            key: Clutter.KEY_Escape,
        });
        this._okButton = this.addButton({
            label: _('Keep Changes'),
            action: this._onSuccess.bind(this),
            default: true,
        });

        this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, ONE_SECOND, this._tick.bind(this));
        GLib.Source.set_name_by_id(this._timeoutId, '[gnome-shell] this._tick');
    }

    close(timestamp) {
        if (this._timeoutId > 0) {
            GLib.source_remove(this._timeoutId);
            this._timeoutId = 0;
        }

        super.close(timestamp);
    }

    _formatCountDown() {
        const fmt = ngettext(
            'Settings changes will revert in %d second',
            'Settings changes will revert in %d seconds',
            this._countDown);
        return fmt.format(this._countDown);
    }

    _tick() {
        this._countDown--;

        if (this._countDown === 0) {
            /* mutter already takes care of failing at timeout */
            this._timeoutId = 0;
            this.close();
            return GLib.SOURCE_REMOVE;
        }

        this._content.description = this._formatCountDown();
        return GLib.SOURCE_CONTINUE;
    }

    _onFailure() {
        this._wm.complete_display_change(false);
        this.close();
    }

    _onSuccess() {
        this._wm.complete_display_change(true);
        this.close();
    }
});

export const WindowDimmer = GObject.registerClass(
class WindowDimmer extends Clutter.BrightnessContrastEffect {
    _init() {
        super._init({
            name: WINDOW_DIMMER_EFFECT_NAME,
            enabled: false,
        });
    }

    _syncEnabled(dimmed) {
        let animating = this.actor.get_transition(`@effects.${this.name}.brightness`) !== null;

        this.enabled = Meta.prefs_get_attach_modal_dialogs() && (animating || dimmed);
    }

    setDimmed(dimmed, animate) {
        let val = 127 * (1 + (dimmed ? 1 : 0) * DIM_BRIGHTNESS);
        let color = Clutter.Color.new(val, val, val, 255);

        this.actor.ease_property(`@effects.${this.name}.brightness`, color, {
            mode: Clutter.AnimationMode.LINEAR,
            duration: (dimmed ? DIM_TIME : UNDIM_TIME) * (animate ? 1 : 0),
            onStopped: () => this._syncEnabled(dimmed),
        });

        this._syncEnabled(dimmed);
    }
});

/**
 * @param {Meta.WindowActor} actor
 */
function getWindowDimmer(actor) {
    let effect = actor.get_effect(WINDOW_DIMMER_EFFECT_NAME);

    if (!effect) {
        effect = new WindowDimmer();
        actor.add_effect(effect);
    }
    return effect;
}

/*
 * When the last window closed on a workspace is a dialog or splash
 * screen, we assume that it might be an initial window shown before
 * the main window of an application, and give the app a grace period
 * where it can map another window before we remove the workspace.
 */
const LAST_WINDOW_GRACE_TIME = 1000;

class WorkspaceTracker {
    constructor(wm) {
        this._wm = wm;

        this._workspaces = [];
        this._checkWorkspacesId = 0;

        this._pauseWorkspaceCheck = false;

        let tracker = Shell.WindowTracker.get_default();
        tracker.connect('startup-sequence-changed', this._queueCheckWorkspaces.bind(this));

        let workspaceManager = global.workspace_manager;
        workspaceManager.connect('notify::n-workspaces',
            this._nWorkspacesChanged.bind(this));
        workspaceManager.connect('workspaces-reordered', () => {
            this._workspaces.sort((a, b) => a.index() - b.index());
        });
        global.window_manager.connect('switch-workspace',
            this._queueCheckWorkspaces.bind(this));

        global.display.connect('window-entered-monitor',
            this._windowEnteredMonitor.bind(this));
        global.display.connect('window-left-monitor',
            this._windowLeftMonitor.bind(this));

        this._workspaceSettings = new Gio.Settings({schema_id: 'org.gnome.mutter'});
        this._workspaceSettings.connect('changed::dynamic-workspaces', this._queueCheckWorkspaces.bind(this));

        this._nWorkspacesChanged();
    }

    blockUpdates() {
        this._pauseWorkspaceCheck = true;
    }

    unblockUpdates() {
        this._pauseWorkspaceCheck = false;
    }

    _checkWorkspaces() {
        let workspaceManager = global.workspace_manager;
        let i;
        let emptyWorkspaces = [];

        if (!Meta.prefs_get_dynamic_workspaces()) {
            this._checkWorkspacesId = 0;
            return false;
        }

        // Update workspaces only if Dynamic Workspace Management has not been paused by some other function
        if (this._pauseWorkspaceCheck)
            return true;

        for (i = 0; i < this._workspaces.length; i++) {
            let lastRemoved = this._workspaces[i]._lastRemovedWindow;
            if ((lastRemoved &&
                 (lastRemoved.get_window_type() === Meta.WindowType.SPLASHSCREEN ||
                  lastRemoved.get_window_type() === Meta.WindowType.DIALOG ||
                  lastRemoved.get_window_type() === Meta.WindowType.MODAL_DIALOG)) ||
                this._workspaces[i]._keepAliveId)
                emptyWorkspaces[i] = false;
            else
                emptyWorkspaces[i] = true;
        }

        let sequences = Shell.WindowTracker.get_default().get_startup_sequences();
        for (i = 0; i < sequences.length; i++) {
            let index = sequences[i].get_workspace();
            if (index >= 0 && index <= workspaceManager.n_workspaces)
                emptyWorkspaces[index] = false;
        }

        let windows = global.get_window_actors();
        for (i = 0; i < windows.length; i++) {
            let actor = windows[i];
            let win = actor.get_meta_window();

            if (win.is_on_all_workspaces())
                continue;

            let workspaceIndex = win.get_workspace().index();
            emptyWorkspaces[workspaceIndex] = false;
        }

        // If we don't have an empty workspace at the end, add one
        if (!emptyWorkspaces[emptyWorkspaces.length - 1]) {
            workspaceManager.append_new_workspace(false, global.get_current_time());
            emptyWorkspaces.push(true);
        }

        // Enforce minimum number of workspaces
        while (emptyWorkspaces.length < MIN_NUM_WORKSPACES) {
            workspaceManager.append_new_workspace(false, global.get_current_time());
            emptyWorkspaces.push(true);
        }

        let lastIndex = emptyWorkspaces.length - 1;
        let lastEmptyIndex = emptyWorkspaces.lastIndexOf(false) + 1;
        let activeWorkspaceIndex = workspaceManager.get_active_workspace_index();
        emptyWorkspaces[activeWorkspaceIndex] = false;

        // Delete empty workspaces except for the last one; do it from the end
        // to avoid index changes
        for (i = lastIndex; i >= 0; i--) {
            if (workspaceManager.n_workspaces === MIN_NUM_WORKSPACES)
                break;
            if (emptyWorkspaces[i] && i !== lastEmptyIndex)
                workspaceManager.remove_workspace(this._workspaces[i], global.get_current_time());
        }

        this._checkWorkspacesId = 0;
        return false;
    }

    keepWorkspaceAlive(workspace, duration) {
        if (workspace._keepAliveId)
            GLib.source_remove(workspace._keepAliveId);

        workspace._keepAliveId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, duration, () => {
            workspace._keepAliveId = 0;
            this._queueCheckWorkspaces();
            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(workspace._keepAliveId, '[gnome-shell] this._queueCheckWorkspaces');
    }

    _windowRemoved(workspace, window) {
        workspace._lastRemovedWindow = window;
        this._queueCheckWorkspaces();
        let id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, LAST_WINDOW_GRACE_TIME, () => {
            if (workspace._lastRemovedWindow === window) {
                workspace._lastRemovedWindow = null;
                this._queueCheckWorkspaces();
            }
            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(id, '[gnome-shell] this._queueCheckWorkspaces');
    }

    _windowLeftMonitor(metaDisplay, monitorIndex, _metaWin) {
        // If the window left the primary monitor, that
        // might make that workspace empty
        if (monitorIndex === Main.layoutManager.primaryIndex)
            this._queueCheckWorkspaces();
    }

    _windowEnteredMonitor(metaDisplay, monitorIndex, _metaWin) {
        // If the window entered the primary monitor, that
        // might make that workspace non-empty
        if (monitorIndex === Main.layoutManager.primaryIndex)
            this._queueCheckWorkspaces();
    }

    _queueCheckWorkspaces() {
        if (this._checkWorkspacesId === 0) {
            const laters = global.compositor.get_laters();
            this._checkWorkspacesId =
                laters.add(Meta.LaterType.BEFORE_REDRAW, this._checkWorkspaces.bind(this));
        }
    }

    _nWorkspacesChanged() {
        let workspaceManager = global.workspace_manager;
        let oldNumWorkspaces = this._workspaces.length;
        let newNumWorkspaces = workspaceManager.n_workspaces;

        if (oldNumWorkspaces === newNumWorkspaces)
            return false;

        if (newNumWorkspaces > oldNumWorkspaces) {
            let w;

            // Assume workspaces are only added at the end
            for (w = oldNumWorkspaces; w < newNumWorkspaces; w++)
                this._workspaces[w] = workspaceManager.get_workspace_by_index(w);

            for (w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
                this._workspaces[w].connectObject(
                    'window-added', this._queueCheckWorkspaces.bind(this),
                    'window-removed', this._windowRemoved.bind(this), this);
            }
        } else {
            // Assume workspaces are only removed sequentially
            // (e.g. 2,3,4 - not 2,4,7)
            let removedIndex;
            let removedNum = oldNumWorkspaces - newNumWorkspaces;
            for (let w = 0; w < oldNumWorkspaces; w++) {
                let workspace = workspaceManager.get_workspace_by_index(w);
                if (this._workspaces[w] !== workspace) {
                    removedIndex = w;
                    break;
                }
            }

            let lostWorkspaces = this._workspaces.splice(removedIndex, removedNum);
            lostWorkspaces.forEach(workspace => workspace.disconnectObject(this));
        }

        this._queueCheckWorkspaces();

        return false;
    }
}

export const TilePreview = GObject.registerClass(
class TilePreview extends St.Widget {
    _init() {
        super._init();
        global.window_group.add_child(this);

        this._reset();
        this._showing = false;
    }

    open(window, tileRect, monitorIndex) {
        let windowActor = window.get_compositor_private();
        if (!windowActor)
            return;

        global.window_group.set_child_below_sibling(this, windowActor);

        if (this._rect && this._rect.equal(tileRect))
            return;

        let changeMonitor = this._monitorIndex === -1 ||
                             this._monitorIndex !== monitorIndex;

        this._monitorIndex = monitorIndex;
        this._rect = tileRect;

        let monitor = Main.layoutManager.monitors[monitorIndex];

        this._updateStyle(monitor);

        if (!this._showing || changeMonitor) {
            const monitorRect = new Mtk.Rectangle({
                x: monitor.x,
                y: monitor.y,
                width: monitor.width,
                height: monitor.height,
            });
            let [, rect] = window.get_frame_rect().intersect(monitorRect);
            this.set_size(rect.width, rect.height);
            this.set_position(rect.x, rect.y);
            this.opacity = 0;
        }

        this._showing = true;
        this.show();
        this.ease({
            x: tileRect.x,
            y: tileRect.y,
            width: tileRect.width,
            height: tileRect.height,
            opacity: 255,
            duration: WINDOW_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    close() {
        if (!this._showing)
            return;

        this._showing = false;
        this.ease({
            opacity: 0,
            duration: WINDOW_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this._reset(),
        });
    }

    _reset() {
        this.hide();
        this._rect = null;
        this._monitorIndex = -1;
    }

    _updateStyle(monitor) {
        let styles = ['tile-preview'];
        if (this._monitorIndex === Main.layoutManager.primaryIndex)
            styles.push('on-primary');
        if (this._rect.x === monitor.x)
            styles.push('tile-preview-left');
        if (this._rect.x + this._rect.width === monitor.x + monitor.width)
            styles.push('tile-preview-right');

        this.style_class = styles.join(' ');
    }
});

const ResizePopup = GObject.registerClass(
class ResizePopup extends St.Widget {
    _init() {
        super._init({layout_manager: new Clutter.BinLayout()});
        this._label = new St.Label({
            style_class: 'resize-popup',
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
            x_expand: true,
            y_expand: true,
        });
        this.add_child(this._label);
        Main.uiGroup.add_child(this);
    }

    set(rect, displayW, displayH) {
        /* Translators: This represents the size of a window. The first number is
         * the width of the window and the second is the height. */
        let text = _('%d × %d').format(displayW, displayH);
        this._label.set_text(text);

        this.set_position(rect.x, rect.y);
        this.set_size(rect.width, rect.height);
    }
});

export class WindowManager {
    constructor() {
        this._shellwm =  global.window_manager;

        this._minimizing = new Set();
        this._unminimizing = new Set();
        this._mapping = new Set();
        this._resizing = new Set();
        this._resizePending = new Set();
        this._destroying = new Set();

        this._skippedActors = new Set();

        this._allowedKeybindings = {};

        this._isWorkspacePrepended = false;
        this._canScroll = true; // limiting scrolling speed

        this._shellwm.connect('kill-window-effects', (shellwm, actor) => {
            this._minimizeWindowDone(shellwm, actor);
            this._mapWindowDone(shellwm, actor);
            this._destroyWindowDone(shellwm, actor);
            this._sizeChangeWindowDone(shellwm, actor);
        });

        this._shellwm.connect('switch-workspace', this._switchWorkspace.bind(this));
        this._shellwm.connect('show-tile-preview', this._showTilePreview.bind(this));
        this._shellwm.connect('hide-tile-preview', this._hideTilePreview.bind(this));
        this._shellwm.connect('show-window-menu', this._showWindowMenu.bind(this));
        this._shellwm.connect('minimize', this._minimizeWindow.bind(this));
        this._shellwm.connect('unminimize', this._unminimizeWindow.bind(this));
        this._shellwm.connect('size-change', this._sizeChangeWindow.bind(this));
        this._shellwm.connect('size-changed', this._sizeChangedWindow.bind(this));
        this._shellwm.connect('map', this._mapWindow.bind(this));
        this._shellwm.connect('destroy', this._destroyWindow.bind(this));
        this._shellwm.connect('filter-keybinding', this._filterKeybinding.bind(this));
        this._shellwm.connect('confirm-display-change', this._confirmDisplayChange.bind(this));
        this._shellwm.connect('create-close-dialog', this._createCloseDialog.bind(this));
        this._shellwm.connect('create-inhibit-shortcuts-dialog', this._createInhibitShortcutsDialog.bind(this));

        this._workspaceSwitcherPopup = null;
        this._tilePreview = null;

        this.allowKeybinding('switch-to-session-1', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-2', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-3', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-4', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-5', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-6', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-7', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-8', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-9', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-10', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-11', Shell.ActionMode.ALL);
        this.allowKeybinding('switch-to-session-12', Shell.ActionMode.ALL);

        this.setCustomKeybindingHandler('switch-to-workspace-left',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-right',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-up',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-down',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-last',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-left',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-right',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-up',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-down',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-1',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-2',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-3',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-4',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-5',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-6',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-7',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-8',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-9',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-10',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-11',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-to-workspace-12',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-1',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-2',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-3',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-4',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-5',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-6',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-7',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-8',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-9',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-10',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-11',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-12',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('move-to-workspace-last',
            Shell.ActionMode.NORMAL,
            this._showWorkspaceSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-applications',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-group',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-applications-backward',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-group-backward',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-windows',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-windows-backward',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('cycle-windows',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('cycle-windows-backward',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('cycle-group',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('cycle-group-backward',
            Shell.ActionMode.NORMAL,
            this._startSwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-panels',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW |
            Shell.ActionMode.LOCK_SCREEN | Shell.ActionMode.UNLOCK_SCREEN |
            Shell.ActionMode.LOGIN_SCREEN,
            this._startA11ySwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-panels-backward',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW |
            Shell.ActionMode.LOCK_SCREEN | Shell.ActionMode.UNLOCK_SCREEN |
            Shell.ActionMode.LOGIN_SCREEN,
            this._startA11ySwitcher.bind(this));

        this.setCustomKeybindingHandler('switch-monitor',
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._startSwitcher.bind(this));

        this.addKeybinding('toggle-message-tray',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW |
            Shell.ActionMode.POPUP,
            this._toggleCalendar.bind(this));

        this.addKeybinding('toggle-quick-settings',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW |
            Shell.ActionMode.POPUP,
            this._toggleQuickSettings.bind(this));

        this.addKeybinding('switch-to-application-1',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._switchToApplication.bind(this));

        this.addKeybinding('switch-to-application-2',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._switchToApplication.bind(this));

        this.addKeybinding('switch-to-application-3',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._switchToApplication.bind(this));

        this.addKeybinding('switch-to-application-4',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._switchToApplication.bind(this));

        this.addKeybinding('switch-to-application-5',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._switchToApplication.bind(this));

        this.addKeybinding('switch-to-application-6',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._switchToApplication.bind(this));

        this.addKeybinding('switch-to-application-7',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._switchToApplication.bind(this));

        this.addKeybinding('switch-to-application-8',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._switchToApplication.bind(this));

        this.addKeybinding('switch-to-application-9',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._switchToApplication.bind(this));

        this.addKeybinding('open-new-window-application-1',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._openNewApplicationWindow.bind(this));

        this.addKeybinding('open-new-window-application-2',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._openNewApplicationWindow.bind(this));

        this.addKeybinding('open-new-window-application-3',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._openNewApplicationWindow.bind(this));

        this.addKeybinding('open-new-window-application-4',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._openNewApplicationWindow.bind(this));

        this.addKeybinding('open-new-window-application-5',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._openNewApplicationWindow.bind(this));

        this.addKeybinding('open-new-window-application-6',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._openNewApplicationWindow.bind(this));

        this.addKeybinding('open-new-window-application-7',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._openNewApplicationWindow.bind(this));

        this.addKeybinding('open-new-window-application-8',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._openNewApplicationWindow.bind(this));

        this.addKeybinding('open-new-window-application-9',
            new Gio.Settings({schema_id: SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._openNewApplicationWindow.bind(this));

        global.stage.connect('scroll-event', (stage, event) => {
            const allowedModes = Shell.ActionMode.NORMAL;
            if ((allowedModes & Main.actionMode) === 0)
                return Clutter.EVENT_PROPAGATE;

            if (this._workspaceAnimation.canHandleScrollEvent(event))
                return Clutter.EVENT_PROPAGATE;

            if ((event.get_state() & global.display.compositor_modifiers) === 0)
                return Clutter.EVENT_PROPAGATE;

            return this.handleWorkspaceScroll(event);
        });

        global.display.connect('show-resize-popup', this._showResizePopup.bind(this));
        global.display.connect('show-pad-osd', this._showPadOsd.bind(this));
        global.display.connect('show-osd', (display, monitorIndex, iconName, label) => {
            let icon = Gio.Icon.new_for_string(iconName);
            Main.osdWindowManager.show(monitorIndex, icon, label, null);
        });

        this._gsdWacomProxy = new GsdWacomProxy(Gio.DBus.session,
            GSD_WACOM_BUS_NAME, GSD_WACOM_OBJECT_PATH,
            (proxy, error) => {
                if (error)
                    log(error.message);
            });

        global.display.connect('pad-mode-switch', (display, pad, _group, _mode) => {
            let labels = [];

            // FIXME: Fix num buttons
            for (let i = 0; i < 50; i++) {
                let str = display.get_pad_action_label(pad, Meta.PadActionType.BUTTON, i);
                labels.push(str ?? '');
            }

            this._gsdWacomProxy?.SetOLEDLabelsAsync(
                pad.get_device_node(), labels).catch(logError);
        });

        global.display.connect('init-xserver', (display, task) => {
            IBusManager.getIBusManager().restartDaemon(['--xim']);

            this._startX11Services(task);

            return true;
        });
        global.display.connect('x11-display-closing', () => {
            if (!Meta.is_wayland_compositor())
                return;

            this._stopX11Services(null);

            IBusManager.getIBusManager().restartDaemon();
        });

        this._windowMenuManager = new WindowMenu.WindowMenuManager();

        if (Main.sessionMode.hasWorkspaces)
            this._workspaceTracker = new WorkspaceTracker(this);

        let mode = Shell.ActionMode.NORMAL;
        let topDragAction = new EdgeDragAction.EdgeDragAction(St.Side.TOP, mode);
        topDragAction.connect('activated',  () => {
            let currentWindow = global.display.focus_window;
            if (currentWindow)
                currentWindow.unmake_fullscreen();
        });

        let updateUnfullscreenGesture = () => {
            let currentWindow = global.display.focus_window;
            topDragAction.enabled = currentWindow && currentWindow.is_fullscreen();
        };

        global.display.connect('notify::focus-window', updateUnfullscreenGesture);
        global.display.connect('in-fullscreen-changed', updateUnfullscreenGesture);
        updateUnfullscreenGesture();

        global.stage.add_action_full('unfullscreen', Clutter.EventPhase.CAPTURE, topDragAction);

        this._workspaceAnimation =
            new WorkspaceAnimation.WorkspaceAnimationController();

        this._shellwm.connect('kill-switch-workspace', () => {
            this._workspaceAnimation.cancelSwitchAnimation();
            this._switchWorkspaceDone();
        });
    }

    async _startX11Services(task) {
        let status = true;
        try {
            await Shell.util_start_systemd_unit(
                'gnome-session-x11-services-ready.target', 'fail', null);
        } catch (e) {
            // Ignore NOT_SUPPORTED error, which indicates we are not systemd
            // managed and gnome-session will have taken care of everything
            // already.
            // Note that we do log cancellation from here.
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_SUPPORTED)) {
                log(`Error starting X11 services: ${e.message}`);
                status = false;
            }
        } finally {
            task.return_boolean(status);
        }
    }

    async _stopX11Services(cancellable) {
        try {
            await Shell.util_stop_systemd_unit(
                'gnome-session-x11-services.target', 'fail', cancellable);
        } catch (e) {
            // Ignore NOT_SUPPORTED error, which indicates we are not systemd
            // managed and gnome-session will have taken care of everything
            // already.
            // Note that we do log cancellation from here.
            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_SUPPORTED))
                log(`Error stopping X11 services: ${e.message}`);
        }
    }

    _showPadOsd(display, device, settings, imagePath, editionMode, monitorIndex) {
        this._currentPadOsd = new PadOsd.PadOsd(device, settings, imagePath, editionMode, monitorIndex);
        this._currentPadOsd.connect('closed', () => (this._currentPadOsd = null));

        return this._currentPadOsd;
    }

    _lookupIndex(windows, metaWindow) {
        for (let i = 0; i < windows.length; i++) {
            if (windows[i].metaWindow === metaWindow)
                return i;
        }
        return -1;
    }

    _switchApp() {
        let windows = global.get_window_actors().filter(actor => {
            let win = actor.metaWindow;
            let workspaceManager = global.workspace_manager;
            let activeWorkspace = workspaceManager.get_active_workspace();
            return !win.is_override_redirect() &&
                    win.located_on_workspace(activeWorkspace);
        });

        if (windows.length === 0)
            return;

        let focusWindow = global.display.focus_window;
        let nextWindow;

        if (focusWindow == null) {
            nextWindow = windows[0].metaWindow;
        } else {
            let index = this._lookupIndex(windows, focusWindow) + 1;

            if (index >= windows.length)
                index = 0;

            nextWindow = windows[index].metaWindow;
        }

        Main.activateWindow(nextWindow);
    }

    insertWorkspace(pos) {
        let workspaceManager = global.workspace_manager;

        if (!Meta.prefs_get_dynamic_workspaces())
            return;

        workspaceManager.append_new_workspace(false, global.get_current_time());

        let windows = global.get_window_actors().map(a => a.meta_window);

        // To create a new workspace, we slide all the windows on workspaces
        // below us to the next workspace, leaving a blank workspace for us
        // to recycle.
        windows.forEach(window => {
            // If the window is attached to an ancestor, we don't need/want
            // to move it
            if (window.get_transient_for() != null)
                return;
            // Same for OR windows
            if (window.is_override_redirect())
                return;
            // Sticky windows don't need moving, in fact moving would
            // unstick them
            if (window.on_all_workspaces)
                return;
            // Windows on workspaces below pos don't need moving
            let index = window.get_workspace().index();
            if (index < pos)
                return;
            window.change_workspace_by_index(index + 1, true);
        });

        // If the new workspace was inserted before the active workspace,
        // activate the workspace to which its windows went
        let activeIndex = workspaceManager.get_active_workspace_index();
        if (activeIndex >= pos) {
            let newWs = workspaceManager.get_workspace_by_index(activeIndex + 1);
            this._blockAnimations = true;
            newWs.activate(global.get_current_time());
            this._blockAnimations = false;
        }
    }

    keepWorkspaceAlive(workspace, duration) {
        if (!this._workspaceTracker)
            return;

        this._workspaceTracker.keepWorkspaceAlive(workspace, duration);
    }

    skipNextEffect(actor) {
        this._skippedActors.add(actor);
    }

    setCustomKeybindingHandler(name, modes, handler) {
        if (Meta.keybindings_set_custom_handler(name, handler))
            this.allowKeybinding(name, modes);
    }

    addKeybinding(name, settings, flags, modes, handler) {
        let action = global.display.add_keybinding(name, settings, flags, handler);
        if (action !== Meta.KeyBindingAction.NONE)
            this.allowKeybinding(name, modes);
        return action;
    }

    removeKeybinding(name) {
        if (global.display.remove_keybinding(name))
            this.allowKeybinding(name, Shell.ActionMode.NONE);
    }

    allowKeybinding(name, modes) {
        this._allowedKeybindings[name] = modes;
    }

    _getAnimationWindowType(actor) {
        const {metaWindow: window} = actor;
        const {windowType} = window;

        if (windowType !== Meta.WindowType.NORMAL ||
            window.get_client_type() === Meta.WindowClientType.X11)
            return windowType;

        // wayland doesn't use the DIALOG type, but for
        // animations we want transients to behave like ones
        return window.get_transient_for() != null
            ? Meta.WindowType.DIALOG
            : windowType;
    }

    _shouldAnimate() {
        const overviewOpen = Main.overview.visible && !Main.overview.closing;
        return !(overviewOpen || this._workspaceAnimation.gestureActive);
    }

    _shouldAnimateActor(actor, types) {
        if (this._skippedActors.delete(actor))
            return false;

        if (!this._shouldAnimate())
            return false;

        if (!actor.get_texture())
            return false;

        const type = this._getAnimationWindowType(actor);
        return types.includes(type);
    }

    _minimizeWindow(shellwm, actor) {
        const types = [
            Meta.WindowType.NORMAL,
            Meta.WindowType.MODAL_DIALOG,
            Meta.WindowType.DIALOG,
        ];
        if (!this._shouldAnimateActor(actor, types)) {
            shellwm.completed_minimize(actor);
            return;
        }

        actor.set_scale(1.0, 1.0);

        this._minimizing.add(actor);

        if (actor.meta_window.is_monitor_sized()) {
            actor.ease({
                opacity: 0,
                duration: MINIMIZE_WINDOW_ANIMATION_TIME,
                mode: MINIMIZE_WINDOW_ANIMATION_MODE,
                onStopped: () => this._minimizeWindowDone(shellwm, actor),
            });
        } else {
            let xDest, yDest, xScale, yScale;
            let [success, geom] = actor.meta_window.get_icon_geometry();
            if (success) {
                xDest = geom.x;
                yDest = geom.y;
                xScale = geom.width / actor.width;
                yScale = geom.height / actor.height;
            } else {
                let monitor = Main.layoutManager.monitors[actor.meta_window.get_monitor()];
                if (!monitor) {
                    this._minimizeWindowDone();
                    return;
                }
                xDest = monitor.x;
                yDest = monitor.y;
                if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
                    xDest += monitor.width;
                xScale = 0;
                yScale = 0;
            }

            actor.ease({
                opacity: 0,
                scale_x: xScale,
                scale_y: yScale,
                x: xDest,
                y: yDest,
                duration: MINIMIZE_WINDOW_ANIMATION_TIME,
                mode: MINIMIZE_WINDOW_ANIMATION_MODE,
                onStopped: () => this._minimizeWindowDone(shellwm, actor),
            });
        }
    }

    _minimizeWindowDone(shellwm, actor) {
        if (this._minimizing.delete(actor)) {
            actor.remove_all_transitions();
            actor.set_scale(1.0, 1.0);
            actor.set_opacity(255);
            actor.set_pivot_point(0, 0);

            shellwm.completed_minimize(actor);
        }
    }

    _unminimizeWindow(shellwm, actor) {
        const types = [
            Meta.WindowType.NORMAL,
            Meta.WindowType.MODAL_DIALOG,
            Meta.WindowType.DIALOG,
        ];
        if (!this._shouldAnimateActor(actor, types)) {
            shellwm.completed_unminimize(actor);
            return;
        }

        this._unminimizing.add(actor);

        if (actor.meta_window.is_monitor_sized()) {
            actor.opacity = 0;
            actor.set_scale(1.0, 1.0);
            actor.ease({
                opacity: 255,
                duration: MINIMIZE_WINDOW_ANIMATION_TIME,
                mode: MINIMIZE_WINDOW_ANIMATION_MODE,
                onStopped: () => this._unminimizeWindowDone(shellwm, actor),
            });
        } else {
            let [success, geom] = actor.meta_window.get_icon_geometry();
            if (success) {
                actor.set_position(geom.x, geom.y);
                actor.set_scale(
                    geom.width / actor.width,
                    geom.height / actor.height);
            } else {
                let monitor = Main.layoutManager.monitors[actor.meta_window.get_monitor()];
                if (!monitor) {
                    actor.show();
                    this._unminimizeWindowDone();
                    return;
                }
                actor.set_position(monitor.x, monitor.y);
                if (Clutter.get_default_text_direction() === Clutter.TextDirection.RTL)
                    actor.x += monitor.width;
                actor.set_scale(0, 0);
            }

            let rect = actor.meta_window.get_buffer_rect();
            let [xDest, yDest] = [rect.x, rect.y];

            actor.show();
            actor.ease({
                scale_x: 1,
                scale_y: 1,
                x: xDest,
                y: yDest,
                duration: MINIMIZE_WINDOW_ANIMATION_TIME,
                mode: MINIMIZE_WINDOW_ANIMATION_MODE,
                onStopped: () => this._unminimizeWindowDone(shellwm, actor),
            });
        }
    }

    _unminimizeWindowDone(shellwm, actor) {
        if (this._unminimizing.delete(actor)) {
            actor.remove_all_transitions();
            actor.set_scale(1.0, 1.0);
            actor.set_opacity(255);
            actor.set_pivot_point(0, 0);

            shellwm.completed_unminimize(actor);
        }
    }

    _sizeChangeWindow(shellwm, actor, whichChange, oldFrameRect, _oldBufferRect) {
        const types = [Meta.WindowType.NORMAL];
        const shouldAnimate =
            this._shouldAnimateActor(actor, types) &&
            oldFrameRect.width > 0 &&
            oldFrameRect.height > 0;

        if (shouldAnimate)
            this._prepareAnimationInfo(shellwm, actor, oldFrameRect, whichChange);
        else
            shellwm.completed_size_change(actor);
    }

    _prepareAnimationInfo(shellwm, actor, oldFrameRect, _change) {
        // Position a clone of the window on top of the old position,
        // while actor updates are frozen.
        let actorContent = actor.paint_to_content(oldFrameRect);
        let actorClone = new St.Widget({content: actorContent});
        actorClone.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);
        actorClone.set_position(oldFrameRect.x, oldFrameRect.y);
        actorClone.set_size(oldFrameRect.width, oldFrameRect.height);

        actor.freeze();

        if (this._clearAnimationInfo(actor)) {
            log(`Old animationInfo removed from actor ${actor}`);
            this._shellwm.completed_size_change(actor);
        }

        actor.connectObject('destroy',
            () => this._clearAnimationInfo(actor), actorClone);

        this._resizePending.add(actor);
        actor.__animationInfo = {
            clone: actorClone,
            oldRect: oldFrameRect,
            frozen: true,
        };
    }

    _sizeChangedWindow(shellwm, actor) {
        if (!actor.__animationInfo)
            return;
        if (this._resizing.has(actor))
            return;

        let actorClone = actor.__animationInfo.clone;
        let targetRect = actor.meta_window.get_frame_rect();
        let sourceRect = actor.__animationInfo.oldRect;

        let scaleX = targetRect.width / sourceRect.width;
        let scaleY = targetRect.height / sourceRect.height;

        this._resizePending.delete(actor);
        this._resizing.add(actor);

        Main.uiGroup.add_child(actorClone);

        // Now scale and fade out the clone
        actorClone.ease({
            x: targetRect.x,
            y: targetRect.y,
            scale_x: scaleX,
            scale_y: scaleY,
            opacity: 0,
            duration: WINDOW_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        actor.translation_x = -targetRect.x + sourceRect.x;
        actor.translation_y = -targetRect.y + sourceRect.y;

        // Now set scale the actor to size it as the clone.
        actor.scale_x = 1 / scaleX;
        actor.scale_y = 1 / scaleY;

        // Scale it to its actual new size
        actor.ease({
            scale_x: 1,
            scale_y: 1,
            translation_x: 0,
            translation_y: 0,
            duration: WINDOW_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onStopped: () => this._sizeChangeWindowDone(shellwm, actor),
        });

        // ease didn't animate and cleared the info, we are done
        if (!actor.__animationInfo)
            return;

        // Now unfreeze actor updates, to get it to the new size.
        // It's important that we don't wait until the animation is completed to
        // do this, otherwise our scale will be applied to the old texture size.
        actor.thaw();
        actor.__animationInfo.frozen = false;
    }

    _clearAnimationInfo(actor) {
        if (actor.__animationInfo) {
            actor.__animationInfo.clone.destroy();
            if (actor.__animationInfo.frozen)
                actor.thaw();

            delete actor.__animationInfo;
            return true;
        }
        return false;
    }

    _sizeChangeWindowDone(shellwm, actor) {
        if (this._resizing.delete(actor)) {
            actor.remove_all_transitions();
            actor.scale_x = 1.0;
            actor.scale_y = 1.0;
            actor.translation_x = 0;
            actor.translation_y = 0;
            this._clearAnimationInfo(actor);
            this._shellwm.completed_size_change(actor);
        }

        if (this._resizePending.delete(actor)) {
            this._clearAnimationInfo(actor);
            this._shellwm.completed_size_change(actor);
        }
    }

    _checkDimming(window) {
        const shouldDim = window.has_attached_dialogs();

        if (shouldDim && !window._dimmed) {
            window._dimmed = true;
            this._dimWindow(window);
        } else if (!shouldDim && window._dimmed) {
            window._dimmed = false;
            this._undimWindow(window);
        }
    }

    _dimWindow(window) {
        let actor = window.get_compositor_private();
        if (!actor)
            return;
        let dimmer = getWindowDimmer(actor);
        if (!dimmer)
            return;
        dimmer.setDimmed(true, this._shouldAnimate());
    }

    _undimWindow(window) {
        let actor = window.get_compositor_private();
        if (!actor)
            return;
        let dimmer = getWindowDimmer(actor);
        if (!dimmer)
            return;
        dimmer.setDimmed(false, this._shouldAnimate());
    }

    _waitForOverviewToHide() {
        if (!Main.overview.visible)
            return Promise.resolve();

        return new Promise(resolve => {
            const id = Main.overview.connect('hidden', () => {
                Main.overview.disconnect(id);
                resolve();
            });
        });
    }

    async _mapWindow(shellwm, actor) {
        actor._windowType = actor.meta_window.get_window_type();
        actor.meta_window.connectObject('notify::window-type', () => {
            let type = actor.meta_window.get_window_type();
            if (type === actor._windowType)
                return;
            if (type === Meta.WindowType.MODAL_DIALOG ||
                actor._windowType === Meta.WindowType.MODAL_DIALOG) {
                let parent = actor.get_meta_window().get_transient_for();
                if (parent)
                    this._checkDimming(parent);
            }

            actor._windowType = type;
        }, actor);
        actor.meta_window.connect('unmanaged', window => {
            let parent = window.get_transient_for();
            if (parent)
                this._checkDimming(parent);
        });

        if (actor.meta_window.is_attached_dialog())
            this._checkDimming(actor.get_meta_window().get_transient_for());

        const types = [
            Meta.WindowType.NORMAL,
            Meta.WindowType.DIALOG,
            Meta.WindowType.MODAL_DIALOG,
        ];
        if (!this._shouldAnimateActor(actor, types)) {
            shellwm.completed_map(actor);
            return;
        }

        switch (this._getAnimationWindowType(actor)) {
        case Meta.WindowType.NORMAL:
            actor.set_pivot_point(0.5, 1.0);
            actor.scale_x = 0.01;
            actor.scale_y = 0.05;
            actor.opacity = 0;
            actor.show();
            this._mapping.add(actor);

            await this._waitForOverviewToHide();
            actor.ease({
                opacity: 255,
                scale_x: 1,
                scale_y: 1,
                duration: SHOW_WINDOW_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_EXPO,
                onStopped: () => this._mapWindowDone(shellwm, actor),
            });
            break;
        case Meta.WindowType.MODAL_DIALOG:
        case Meta.WindowType.DIALOG:
            actor.set_pivot_point(0.5, 0.5);
            actor.scale_y = 0;
            actor.opacity = 0;
            actor.show();
            this._mapping.add(actor);

            await this._waitForOverviewToHide();
            actor.ease({
                opacity: 255,
                scale_x: 1,
                scale_y: 1,
                duration: DIALOG_SHOW_WINDOW_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onStopped: () => this._mapWindowDone(shellwm, actor),
            });
            break;
        default:
            shellwm.completed_map(actor);
        }
    }

    _mapWindowDone(shellwm, actor) {
        if (this._mapping.delete(actor)) {
            actor.remove_all_transitions();
            actor.opacity = 255;
            actor.set_pivot_point(0, 0);
            actor.scale_y = 1;
            actor.scale_x = 1;
            actor.translation_y = 0;
            actor.translation_x = 0;
            shellwm.completed_map(actor);
        }
    }

    _destroyWindow(shellwm, actor) {
        let window = actor.meta_window;
        window.disconnectObject(actor);

        if (window.is_attached_dialog())
            this._checkDimming(window.get_transient_for());

        const types = [
            Meta.WindowType.NORMAL,
            Meta.WindowType.DIALOG,
            Meta.WindowType.MODAL_DIALOG,
        ];
        if (!this._shouldAnimateActor(actor, types)) {
            shellwm.completed_destroy(actor);
            return;
        }

        switch (this._getAnimationWindowType(actor)) {
        case Meta.WindowType.NORMAL:
            actor.set_pivot_point(0.5, 0.5);
            this._destroying.add(actor);

            actor.ease({
                opacity: 0,
                scale_x: 0.8,
                scale_y: 0.8,
                duration: DESTROY_WINDOW_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onStopped: () => this._destroyWindowDone(shellwm, actor),
            });
            break;
        case Meta.WindowType.MODAL_DIALOG:
        case Meta.WindowType.DIALOG:
            actor.set_pivot_point(0.5, 0.5);
            this._destroying.add(actor);

            if (window.is_attached_dialog()) {
                let parent = window.get_transient_for();
                parent.connectObject('unmanaged', () => {
                    actor.remove_all_transitions();
                    this._destroyWindowDone(shellwm, actor);
                }, actor);
            }

            actor.ease({
                scale_y: 0,
                duration: DIALOG_DESTROY_WINDOW_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onStopped: () => this._destroyWindowDone(shellwm, actor),
            });
            break;
        default:
            shellwm.completed_destroy(actor);
        }
    }

    _destroyWindowDone(shellwm, actor) {
        if (this._destroying.delete(actor)) {
            const parent = actor.get_meta_window()?.get_transient_for();
            parent?.disconnectObject(actor);
            shellwm.completed_destroy(actor);
        }
    }

    _filterKeybinding(shellwm, binding) {
        if (Main.actionMode === Shell.ActionMode.NONE)
            return true;

        // There's little sense in implementing a keybinding in mutter and
        // not having it work in NORMAL mode; handle this case generically
        // so we don't have to explicitly allow all builtin keybindings in
        // NORMAL mode.
        if (Main.actionMode === Shell.ActionMode.NORMAL &&
            binding.is_builtin())
            return false;

        return !(this._allowedKeybindings[binding.get_name()] & Main.actionMode);
    }

    _switchWorkspace(shellwm, from, to, direction) {
        if (!Main.sessionMode.hasWorkspaces || !this._shouldAnimate()) {
            shellwm.completed_switch_workspace();
            return;
        }

        this._switchInProgress = true;

        this._workspaceAnimation.animateSwitch(from, to, direction, () => {
            this._shellwm.completed_switch_workspace();
            this._switchInProgress = false;
        });
    }

    _switchWorkspaceDone() {
        if (!this._switchInProgress)
            return;

        this._shellwm.completed_switch_workspace();
        this._switchInProgress = false;
    }

    _showTilePreview(shellwm, window, tileRect, monitorIndex) {
        if (!this._tilePreview)
            this._tilePreview = new TilePreview();
        this._tilePreview.open(window, tileRect, monitorIndex);
    }

    _hideTilePreview() {
        if (!this._tilePreview)
            return;
        this._tilePreview.close();
    }

    _showWindowMenu(shellwm, window, menu, rect) {
        this._windowMenuManager.showWindowMenuForWindow(window, menu, rect);
    }

    _startSwitcher(display, window, binding) {
        let constructor = null;
        switch (binding.get_name()) {
        case 'switch-applications':
        case 'switch-applications-backward':
        case 'switch-group':
        case 'switch-group-backward':
            constructor = AltTab.AppSwitcherPopup;
            break;
        case 'switch-windows':
        case 'switch-windows-backward':
            constructor = AltTab.WindowSwitcherPopup;
            break;
        case 'cycle-windows':
        case 'cycle-windows-backward':
            constructor = AltTab.WindowCyclerPopup;
            break;
        case 'cycle-group':
        case 'cycle-group-backward':
            constructor = AltTab.GroupCyclerPopup;
            break;
        case 'switch-monitor':
            constructor = SwitchMonitor.SwitchMonitorPopup;
            break;
        }

        if (!constructor)
            return;

        /* prevent a corner case where both popups show up at once */
        if (this._workspaceSwitcherPopup != null)
            this._workspaceSwitcherPopup.destroy();

        let tabPopup = new constructor();

        if (!tabPopup.show(binding.is_reversed(), binding.get_name(), binding.get_mask()))
            tabPopup.destroy();
    }

    _startA11ySwitcher(display, window, binding) {
        Main.ctrlAltTabManager.popup(binding.is_reversed(), binding.get_name(), binding.get_mask());
    }

    _allowFavoriteShortcuts() {
        return Main.sessionMode.hasOverview;
    }

    _getNthFavoriteApp(n) {
        if (!this._allowFavoriteShortcuts())
            return null;

        const apps = AppFavorites.getAppFavorites().getFavorites();
        return apps[n];
    }

    _switchToApplication(display, window, binding) {
        const [, , , target] = binding.get_name().split('-');
        const app = this._getNthFavoriteApp(target - 1);
        if (app) {
            Main.overview.hide();
            app.activate();
        }
    }

    _openNewApplicationWindow(display, window, binding) {
        const [, , , , target] = binding.get_name().split('-');
        const app = this._getNthFavoriteApp(target - 1);
        if (app)
            app.open_new_window(-1);
    }

    _toggleCalendar() {
        Main.panel.toggleCalendar();
    }

    _toggleQuickSettings() {
        Main.panel.toggleQuickSettings();
    }

    _showWorkspaceSwitcher(display, window, binding) {
        let workspaceManager = display.get_workspace_manager();

        if (!Main.sessionMode.hasWorkspaces)
            return;

        if (workspaceManager.n_workspaces === 1)
            return;

        let [action,,, target] = binding.get_name().split('-');
        let newWs;
        let direction;
        let vertical = workspaceManager.layout_rows === -1;
        let rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;

        if (action === 'move') {
            // "Moving" a window to another workspace doesn't make sense when
            // it cannot be unstuck, and is potentially confusing if a new
            // workspaces is added at the start/end
            if (window.is_always_on_all_workspaces() ||
                (Meta.prefs_get_workspaces_only_on_primary() &&
                 window.get_monitor() !== Main.layoutManager.primaryIndex))
                return;
        }

        if (target === 'last') {
            if (vertical)
                direction = Meta.MotionDirection.DOWN;
            else if (rtl)
                direction = Meta.MotionDirection.LEFT;
            else
                direction = Meta.MotionDirection.RIGHT;
            newWs = workspaceManager.get_workspace_by_index(workspaceManager.n_workspaces - 1);
        } else if (isNaN(target)) {
            // Prepend a new workspace dynamically
            let prependTarget;
            if (vertical)
                prependTarget = 'up';
            else if (rtl)
                prependTarget = 'right';
            else
                prependTarget = 'left';
            if (workspaceManager.get_active_workspace_index() === 0 &&
                action === 'move' && target === prependTarget &&
                this._isWorkspacePrepended === false) {
                this.insertWorkspace(0);
                this._isWorkspacePrepended = true;
            }

            direction = Meta.MotionDirection[target.toUpperCase()];
            newWs = workspaceManager.get_active_workspace().get_neighbor(direction);
        } else if ((target > 0) && (target <= workspaceManager.n_workspaces)) {
            target--;
            newWs = workspaceManager.get_workspace_by_index(target);

            if (workspaceManager.get_active_workspace_index() > target) {
                if (vertical)
                    direction = Meta.MotionDirection.UP;
                else if (rtl)
                    direction = Meta.MotionDirection.RIGHT;
                else
                    direction = Meta.MotionDirection.LEFT;
            } else {
                if (vertical) // eslint-disable-line no-lonely-if
                    direction = Meta.MotionDirection.DOWN;
                else if (rtl)
                    direction = Meta.MotionDirection.LEFT;
                else
                    direction = Meta.MotionDirection.RIGHT;
            }
        }

        if (workspaceManager.layout_rows === -1 &&
            direction !== Meta.MotionDirection.UP &&
            direction !== Meta.MotionDirection.DOWN)
            return;

        if (workspaceManager.layout_columns === -1 &&
            direction !== Meta.MotionDirection.LEFT &&
            direction !== Meta.MotionDirection.RIGHT)
            return;

        if (action === 'switch')
            this.actionMoveWorkspace(newWs);
        else
            this.actionMoveWindow(window, newWs);

        if (!Main.overview.visible) {
            if (this._workspaceSwitcherPopup == null) {
                this._workspaceTracker.blockUpdates();
                this._workspaceSwitcherPopup = new WorkspaceSwitcherPopup.WorkspaceSwitcherPopup();
                this._workspaceSwitcherPopup.connect('destroy', () => {
                    this._workspaceTracker.unblockUpdates();
                    this._workspaceSwitcherPopup = null;
                    this._isWorkspacePrepended = false;
                });
            }
            this._workspaceSwitcherPopup.display(newWs.index());
        }
    }

    actionMoveWorkspace(workspace) {
        if (!Main.sessionMode.hasWorkspaces)
            return;

        if (!workspace.active)
            workspace.activate(global.get_current_time());
    }

    actionMoveWindow(window, workspace) {
        if (!Main.sessionMode.hasWorkspaces)
            return;

        if (!workspace.active) {
            // This won't have any effect for "always sticky" windows
            // (like desktop windows or docks)

            this._workspaceAnimation.movingWindow = window;
            window.change_workspace(workspace);

            global.display.clear_mouse_mode();
            workspace.activate_with_focus(window, global.get_current_time());
        }
    }

    handleWorkspaceScroll(event) {
        if (!this._canScroll)
            return Clutter.EVENT_PROPAGATE;

        if (event.type() !== Clutter.EventType.SCROLL)
            return Clutter.EVENT_PROPAGATE;

        const direction = event.get_scroll_direction();
        if (direction === Clutter.ScrollDirection.SMOOTH)
            return Clutter.EVENT_PROPAGATE;

        const workspaceManager = global.workspace_manager;
        const vertical = workspaceManager.layout_rows === -1;
        const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL;
        const activeWs = workspaceManager.get_active_workspace();
        let ws;
        switch (direction) {
        case Clutter.ScrollDirection.UP:
            if (vertical)
                ws = activeWs.get_neighbor(Meta.MotionDirection.UP);
            else if (rtl)
                ws = activeWs.get_neighbor(Meta.MotionDirection.RIGHT);
            else
                ws = activeWs.get_neighbor(Meta.MotionDirection.LEFT);
            break;
        case Clutter.ScrollDirection.DOWN:
            if (vertical)
                ws = activeWs.get_neighbor(Meta.MotionDirection.DOWN);
            else if (rtl)
                ws = activeWs.get_neighbor(Meta.MotionDirection.LEFT);
            else
                ws = activeWs.get_neighbor(Meta.MotionDirection.RIGHT);
            break;
        case Clutter.ScrollDirection.LEFT:
            ws = activeWs.get_neighbor(Meta.MotionDirection.LEFT);
            break;
        case Clutter.ScrollDirection.RIGHT:
            ws = activeWs.get_neighbor(Meta.MotionDirection.RIGHT);
            break;
        default:
            return Clutter.EVENT_PROPAGATE;
        }
        this.actionMoveWorkspace(ws);

        this._canScroll = false;
        GLib.timeout_add(GLib.PRIORITY_DEFAULT,
            SCROLL_TIMEOUT_TIME, () => {
                this._canScroll = true;
                return GLib.SOURCE_REMOVE;
            });

        return Clutter.EVENT_STOP;
    }

    _confirmDisplayChange() {
        let dialog = new DisplayChangeDialog(this._shellwm);
        dialog.open();
    }

    _createCloseDialog(shellwm, window) {
        return new CloseDialog.CloseDialog(window);
    }

    _createInhibitShortcutsDialog(shellwm, window) {
        return new InhibitShortcutsDialog.InhibitShortcutsDialog(window);
    }

    _showResizePopup(display, show, rect, displayW, displayH) {
        if (show) {
            if (!this._resizePopup)
                this._resizePopup = new ResizePopup();

            this._resizePopup.set(rect, displayW, displayH);
        } else {
            if (!this._resizePopup)
                return;

            this._resizePopup.destroy();
            this._resizePopup = null;
        }
    }
}
(uuay)const.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

export const WEB_LOGIN_ROLE_NAME = 'eidp';
export const PLAIN_TEXT_ROLE_NAME = 'text';
export const MESSAGE_ROLE_NAME = 'message';
export const PASSWORD_ROLE_NAME = 'password';
export const SMARTCARD_ROLE_NAME = 'smartcard';
export const FINGERPRINT_ROLE_NAME = 'fingerprint';
export const CHOICE_LIST_ROLE_NAME = 'choice-list';
(uuay)extensionUtils.jsk// Common utils for the extension system, the extensions D-Bus service
// and the Extensions app

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';

export const ExtensionType = {
    SYSTEM: 1,
    PER_USER: 2,
};

/**
 * @enum {number}
 */
export const ExtensionState = {
    ACTIVE: 1,
    INACTIVE: 2,
    ERROR: 3,
    OUT_OF_DATE: 4,
    DOWNLOADING: 5,
    INITIALIZED: 6,
    DEACTIVATING: 7,
    ACTIVATING: 8,

    // Used as an error state for operations on unknown extensions,
    // should never be in a real extensionMeta object.
    UNINSTALLED: 99,
};

const SERIALIZED_PROPERTIES = [
    'type',
    'state',
    'enabled',
    'path',
    'error',
    'hasPrefs',
    'hasUpdate',
    'canChange',
];

/**
 * Serialize extension into an object that can be used
 * in a vardict {GLib.Variant}
 *
 * @param {object} extension - an extension object
 * @returns {object}
 */
export function serializeExtension(extension) {
    let obj = {...extension.metadata};

    SERIALIZED_PROPERTIES.forEach(prop => {
        obj[prop] = extension[prop];
    });

    let res = {};
    for (let key in obj) {
        let val = obj[key];
        let type;
        switch (typeof val) {
        case 'string':
            type = 's';
            break;
        case 'number':
            type = 'd';
            break;
        case 'boolean':
            type = 'b';
            break;
        default:
            continue;
        }
        res[key] = GLib.Variant.new(type, val);
    }

    return res;
}

/**
 * Deserialize an unpacked variant into an extension object
 *
 * @param {object} variant - an unpacked {GLib.Variant}
 * @returns {object}
 */
export function deserializeExtension(variant) {
    let res = {metadata: {}};
    for (let prop in variant) {
        let val = variant[prop].unpack();
        if (SERIALIZED_PROPERTIES.includes(prop))
            res[prop] = val;
        else
            res.metadata[prop] = val;
    }
    // add the 2 additional properties to create a valid extension object, as createExtensionObject()
    res.uuid = res.metadata.uuid;
    res.dir = Gio.File.new_for_path(res.path);
    return res;
}
(uuay)grabHelper.js�$// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import St from 'gi://St';

import * as Main from './main.js';
import * as Params from '../misc/params.js';

/**
 * GrabHelper:
 *
 * Creates a new GrabHelper object, for dealing with keyboard and pointer grabs
 * associated with a set of actors.
 *
 * Note that the grab can be automatically dropped at any time by the user, and
 * your code just needs to deal with it; you shouldn't adjust behavior directly
 * after you call ungrab(), but instead pass an 'onUngrab' callback when you
 * call grab().
 */
export class GrabHelper {
    /**
     * @param {Clutter.Actor} owner the actor that owns the GrabHelper
     * @param {*} params optional parameters to pass to Main.pushModal()
     */
    constructor(owner, params) {
        if (!(owner instanceof Clutter.Actor))
            throw new Error('GrabHelper owner must be a Clutter.Actor');

        this._owner = owner;
        this._modalParams = params;

        this._grabStack = [];

        this._ignoreUntilRelease = false;

        this._modalCount = 0;
    }

    _isWithinGrabbedActor(actor) {
        let currentActor = this.currentGrab.actor;
        while (actor) {
            if (actor === currentActor)
                return true;
            actor = actor.get_parent();
        }
        return false;
    }

    get currentGrab() {
        return this._grabStack[this._grabStack.length - 1] || {};
    }

    get grabbed() {
        return this._grabStack.length > 0;
    }

    get grabStack() {
        return this._grabStack;
    }

    _findStackIndex(actor) {
        if (!actor)
            return -1;

        for (let i = 0; i < this._grabStack.length; i++) {
            if (this._grabStack[i].actor === actor)
                return i;
        }
        return -1;
    }

    _actorInGrabStack(actor) {
        while (actor) {
            let idx = this._findStackIndex(actor);
            if (idx >= 0)
                return idx;
            actor = actor.get_parent();
        }
        return -1;
    }

    isActorGrabbed(actor) {
        return this._findStackIndex(actor) >= 0;
    }

    // grab:
    // @params: A bunch of parameters, see below
    //
    // The general effect of a "grab" is to ensure that the passed in actor
    // and all actors inside the grab get exclusive control of the mouse and
    // keyboard, with the grab automatically being dropped if the user tries
    // to dismiss it. The actor is passed in through @params.actor.
    //
    // grab() can be called multiple times, with the scope of the grab being
    // changed to a different actor every time. A nested grab does not have
    // to have its grabbed actor inside the parent grab actors.
    //
    // Grabs can be automatically dropped if the user tries to dismiss it
    // in one of two ways: the user clicking outside the currently grabbed
    // actor, or the user typing the Escape key.
    //
    // If the user clicks outside the grabbed actors, and the clicked on
    // actor is part of a previous grab in the stack, grabs will be popped
    // until that grab is active. However, the click event will not be
    // replayed to the actor.
    //
    // If the user types the Escape key, one grab from the grab stack will
    // be popped.
    //
    // When a grab is popped by user interacting as described above, if you
    // pass a callback as @params.onUngrab, it will be called with %true.
    //
    // If @params.focus is not null, we'll set the key focus directly
    // to that actor instead of navigating in @params.actor. This is for
    // use cases like menus, where we want to grab the menu actor, but keep
    // focus on the clicked on menu item.
    grab(params) {
        params = Params.parse(params, {
            actor: null,
            focus: null,
            onUngrab: null,
        });

        let focus = global.stage.key_focus;
        let hadFocus = focus && this._isWithinGrabbedActor(focus);
        let newFocus = params.actor;

        if (this.isActorGrabbed(params.actor))
            return true;

        params.savedFocus = focus;

        if (!this._takeModalGrab())
            return false;

        this._grabStack.push(params);

        if (params.focus) {
            params.focus.grab_key_focus();
        } else if (newFocus && hadFocus) {
            if (!newFocus.navigate_focus(null, St.DirectionType.TAB_FORWARD, false))
                newFocus.grab_key_focus();
        }

        return true;
    }

    grabAsync(params) {
        return new Promise((resolve, reject) => {
            params.onUngrab = resolve;

            if (!this.grab(params))
                reject(new Error('Grab failed'));
        });
    }

    _takeModalGrab() {
        let firstGrab = this._modalCount === 0;
        if (firstGrab) {
            let grab = Main.pushModal(this._owner, this._modalParams);
            if (grab.get_seat_state() !== Clutter.GrabState.ALL) {
                Main.popModal(grab);
                return false;
            }

            this._grab = grab;
            this._capturedEventId = this._owner.connect('captured-event',
                (actor, event) => {
                    return this.onCapturedEvent(event);
                });
        }

        this._modalCount++;
        return true;
    }

    _releaseModalGrab() {
        this._modalCount--;
        if (this._modalCount > 0)
            return;

        this._owner.disconnect(this._capturedEventId);
        this._ignoreUntilRelease = false;

        Main.popModal(this._grab);
        this._grab = null;
    }

    // ignoreRelease:
    //
    // Make sure that the next button release event evaluated by the
    // capture event handler returns false. This is designed for things
    // like the ComboBoxMenu that go away on press, but need to eat
    // the next release event.
    ignoreRelease() {
        this._ignoreUntilRelease = true;
    }

    // ungrab:
    // @params: The parameters for the grab; see below.
    //
    // Pops @params.actor from the grab stack, potentially dropping
    // the grab. If the actor is not on the grab stack, this call is
    // ignored with no ill effects.
    //
    // If the actor is not at the top of the grab stack, grabs are
    // popped until the grabbed actor is at the top of the grab stack.
    // The onUngrab callback for every grab is called for every popped
    // grab with the parameter %false.
    ungrab(params) {
        params = Params.parse(params, {
            actor: this.currentGrab.actor,
            isUser: false,
        });

        let grabStackIndex = this._findStackIndex(params.actor);
        if (grabStackIndex < 0)
            return;

        let focus = global.stage.key_focus;
        let hadFocus = focus && this._isWithinGrabbedActor(focus);

        let poppedGrabs = this._grabStack.slice(grabStackIndex);
        // "Pop" all newly ungrabbed actors off the grab stack
        // by truncating the array.
        this._grabStack.length = grabStackIndex;

        for (let i = poppedGrabs.length - 1; i >= 0; i--) {
            let poppedGrab = poppedGrabs[i];

            if (poppedGrab.onUngrab)
                poppedGrab.onUngrab(params.isUser);

            this._releaseModalGrab();
        }

        if (hadFocus) {
            let poppedGrab = poppedGrabs[0];
            if (poppedGrab.savedFocus)
                poppedGrab.savedFocus.grab_key_focus();
        }
    }

    onCapturedEvent(event) {
        let type = event.type();

        if (type === Clutter.EventType.KEY_PRESS &&
            event.get_key_symbol() === Clutter.KEY_Escape) {
            this.ungrab({isUser: true});
            return Clutter.EVENT_STOP;
        }

        let motion = type === Clutter.EventType.MOTION;
        let press = type === Clutter.EventType.BUTTON_PRESS;
        let release = type === Clutter.EventType.BUTTON_RELEASE;
        let button = press || release;

        let touchUpdate = type === Clutter.EventType.TOUCH_UPDATE;
        let touchBegin = type === Clutter.EventType.TOUCH_BEGIN;
        let touchEnd = type === Clutter.EventType.TOUCH_END;
        let touch = touchUpdate || touchBegin || touchEnd;

        if (touch && !global.display.is_pointer_emulating_sequence(event.get_event_sequence()))
            return Clutter.EVENT_PROPAGATE;

        if (this._ignoreUntilRelease && (motion || release || touch)) {
            if (release || touchEnd)
                this._ignoreUntilRelease = false;
            return Clutter.EVENT_PROPAGATE;
        }

        const targetActor = global.stage.get_event_actor(event);

        if (type === Clutter.EventType.ENTER ||
            type === Clutter.EventType.LEAVE ||
            this.currentGrab.actor.contains(targetActor))
            return Clutter.EVENT_PROPAGATE;

        if (Main.keyboard.maybeHandleEvent(event))
            return Clutter.EVENT_PROPAGATE;

        if (button || touchBegin) {
            // If we have a press event, ignore the next
            // motion/release events.
            if (press || touchBegin)
                this._ignoreUntilRelease = true;

            let i = this._actorInGrabStack(targetActor) + 1;
            this.ungrab({actor: this._grabStack[i].actor, isUser: true});
            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_STOP;
    }
}
(uuay)thunderbolt.js�'// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

// the following is a modified version of bolt/contrib/js/client.js

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Polkit from 'gi://Polkit';
import Shell from 'gi://Shell';
import * as Signals from '../../misc/signals.js';

import * as Main from '../main.js';
import * as MessageTray from '../messageTray.js';
import {SystemIndicator} from '../quickSettings.js';

import {loadInterfaceXML} from '../../misc/fileUtils.js';

/* Keep in sync with data/org.freedesktop.bolt.xml */

const BoltClientInterface = loadInterfaceXML('org.freedesktop.bolt1.Manager');
const BoltDeviceInterface = loadInterfaceXML('org.freedesktop.bolt1.Device');

const BoltDeviceProxy = Gio.DBusProxy.makeProxyWrapper(BoltDeviceInterface);

/** @enum {string} */
const Status = {
    DISCONNECTED: 'disconnected',
    CONNECTING: 'connecting',
    CONNECTED: 'connected',
    AUTHORIZING: 'authorizing',
    AUTH_ERROR: 'auth-error',
    AUTHORIZED: 'authorized',
};

/** @enum {string} */
const Policy = {
    DEFAULT: 'default',
    MANUAL: 'manual',
    AUTO: 'auto',
};

/** @enum {string} */
const AuthCtrl = {
    NONE: 'none',
};

/** @enum {string} */
const AuthMode = {
    DISABLED: 'disabled',
    ENABLED: 'enabled',
};

const BOLT_DBUS_CLIENT_IFACE = 'org.freedesktop.bolt1.Manager';
const BOLT_DBUS_NAME = 'org.freedesktop.bolt';
const BOLT_DBUS_PATH = '/org/freedesktop/bolt';

class Client extends Signals.EventEmitter {
    constructor() {
        super();

        this._proxy = null;
        this.probing = false;
        this._getProxy();
    }

    async _getProxy() {
        let nodeInfo = Gio.DBusNodeInfo.new_for_xml(BoltClientInterface);
        try {
            this._proxy = await Gio.DBusProxy.new(
                Gio.DBus.system,
                Gio.DBusProxyFlags.DO_NOT_AUTO_START,
                nodeInfo.lookup_interface(BOLT_DBUS_CLIENT_IFACE),
                BOLT_DBUS_NAME,
                BOLT_DBUS_PATH,
                BOLT_DBUS_CLIENT_IFACE,
                null);
        } catch (e) {
            log(`error creating bolt proxy: ${e.message}`);
            return;
        }
        this._proxy.connectObject('g-properties-changed',
            this._onPropertiesChanged.bind(this), this);
        this._deviceAddedId = this._proxy.connectSignal('DeviceAdded', this._onDeviceAdded.bind(this));

        this.probing = this._proxy.Probing;
        if (this.probing)
            this.emit('probing-changed', this.probing);
    }

    _onPropertiesChanged(proxy, properties) {
        const probingChanged = !!properties.lookup_value('Probing', null);
        if (probingChanged) {
            this.probing = this._proxy.Probing;
            this.emit('probing-changed', this.probing);
        }
    }

    _onDeviceAdded(proxy, emitter, params) {
        let [path] = params;
        let device = new BoltDeviceProxy(Gio.DBus.system, BOLT_DBUS_NAME, path);
        this.emit('device-added', device);
    }

    /* public methods */
    close() {
        if (!this._proxy)
            return;

        this._proxy.disconnectSignal(this._deviceAddedId);
        this._proxy.disconnectObject(this);
        this._proxy = null;
    }

    async enrollDevice(id, policy) {
        try {
            const [path] = await this._proxy.EnrollDeviceAsync(id, policy, AuthCtrl.NONE);
            const device = new BoltDeviceProxy(Gio.DBus.system, BOLT_DBUS_NAME, path);
            return device;
        } catch (error) {
            Gio.DBusError.strip_remote_error(error);
            throw error;
        }
    }

    get authMode() {
        return this._proxy.AuthMode;
    }
}

/* helper class to automatically authorize new devices */
class AuthRobot extends Signals.EventEmitter {
    constructor(client) {
        super();

        this._client = client;

        this._devicesToEnroll = [];
        this._enrolling = false;

        this._client.connect('device-added', this._onDeviceAdded.bind(this));
    }

    close() {
        this.disconnectAll();
        this._client = null;
    }

    /* the "device-added" signal will be emitted by boltd for every
     * device that is not currently stored in the database. We are
     * only interested in those devices, because all known devices
     * will be handled by the user himself */
    _onDeviceAdded(cli, dev) {
        if (dev.Status !== Status.CONNECTED)
            return;

        /* check if authorization is enabled in the daemon. if not
         * we won't even bother authorizing, because we will only
         * get an error back. The exact contents of AuthMode might
         * change in the future, but must contain AuthMode.ENABLED
         * if it is enabled. */
        if (!cli.authMode.split('|').includes(AuthMode.ENABLED))
            return;

        /* check if we should enroll the device */
        let res = [false];
        this.emit('enroll-device', dev, res);
        if (res[0] !== true)
            return;

        /* ok, we should authorize the device, add it to the back
         * of the list  */
        this._devicesToEnroll.push(dev);
        this._enrollDevices();
    }

    /* The enrollment queue:
     *   - new devices will be added to the end of the array.
     *   - an idle callback will be scheduled that will keep
     *     calling itself as long as there a devices to be
     *     enrolled.
     */
    _enrollDevices() {
        if (this._enrolling)
            return;

        this._enrolling = true;
        GLib.idle_add(GLib.PRIORITY_DEFAULT,
            this._enrollDevicesIdle.bind(this));
    }

    async _enrollDevicesIdle() {
        let devices = this._devicesToEnroll;

        let dev = devices.shift();
        if (dev === undefined)
            return GLib.SOURCE_REMOVE;

        try {
            await this._client.enrollDevice(dev.Uid, Policy.DEFAULT);

            /* TODO: scan the list of devices to be authorized for children
             *  of this device and remove them (and their children and
             *  their children and ....) from the device queue
             */
            this._enrolling = this._devicesToEnroll.length > 0;

            if (this._enrolling) {
                GLib.idle_add(GLib.PRIORITY_DEFAULT,
                    this._enrollDevicesIdle.bind(this));
            }
        } catch (error) {
            this.emit('enroll-failed', null, error);
        }
        return GLib.SOURCE_REMOVE;
    }
}

/* eof client.js  */

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'thunderbolt-symbolic';

        this._client = new Client();
        this._client.connect('probing-changed', this._onProbing.bind(this));

        this._robot =  new AuthRobot(this._client);

        this._robot.connect('enroll-device', this._onEnrollDevice.bind(this));
        this._robot.connect('enroll-failed', this._onEnrollFailed.bind(this));

        Main.sessionMode.connect('updated', this._sync.bind(this));
        this._sync();

        this._perm = null;
        this._createPermission();
    }

    async _createPermission() {
        try {
            this._perm = await Polkit.Permission.new('org.freedesktop.bolt.enroll', null, null);
        } catch (e) {
            log(`Failed to get PolKit permission: ${e}`);
        }
    }

    _onDestroy() {
        this._robot.close();
        this._client.close();
    }

    _notify(title, body) {
        if (this._notification)
            this._notification.destroy();

        const source = MessageTray.getSystemSource();
        this._notification = new MessageTray.Notification({
            source,
            title,
            body,
            iconName: 'thunderbolt-symbolic',
            urgency: MessageTray.Urgency.HIGH,
        });
        this._notification.connect('destroy', () => {
            this._notification = null;
        });
        this._notification.connect('activated', () => {
            const app = Shell.AppSystem.get_default().lookup_app('gnome-thunderbolt-panel.desktop');
            app?.activate();
        });
        source.addNotification(this._notification);
    }

    /* Session callbacks */
    _sync() {
        let active = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
        this._indicator.visible = active && this._client.probing;
    }

    /* Bolt.Client callbacks */
    _onProbing(cli, probing) {
        if (probing)
            this._indicator.icon_name = 'thunderbolt-acquiring-symbolic';
        else
            this._indicator.icon_name = 'thunderbolt-symbolic';

        this._sync();
    }

    /* AuthRobot callbacks */
    _onEnrollDevice(obj, device, policy) {
        /* only authorize new devices when in an unlocked user session */
        let unlocked = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
        /* and if we have the permission to do so, otherwise we trigger a PolKit dialog */
        let allowed = this._perm && this._perm.allowed;

        let auth = unlocked && allowed;
        policy[0] = auth;

        log(`thunderbolt: [${device.Name}] auto enrollment: ${auth ? 'yes' : 'no'} (allowed: ${allowed ? 'yes' : 'no'})`);

        if (auth)
            return; /* we are done */

        if (!unlocked) {
            const title = _('Unknown Thunderbolt device');
            const body = _('New device has been detected while you were away. Please disconnect and reconnect the device to start using it.');
            this._notify(title, body);
        } else {
            const title = _('Unauthorized Thunderbolt device');
            const body = _('New device has been detected and needs to be authorized by an administrator.');
            this._notify(title, body);
        }
    }

    _onEnrollFailed(obj, device, error) {
        const title = _('Thunderbolt authorization error');
        const body = _('Could not authorize the Thunderbolt device: %s').format(error.message);
        this._notify(title, body);
    }
});
(uuay)animation.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import St from 'gi://St';

import * as Params from '../misc/params.js';

const ANIMATED_ICON_UPDATE_TIMEOUT = 16;
const SPINNER_ANIMATION_TIME = 300;
const SPINNER_ANIMATION_DELAY = 1000;
const SPINNER_ANIMATION_CELL_SIZE = 16;

export const Animation = GObject.registerClass(
class Animation extends St.Bin {
    _init(params) {
        const themeContext = St.ThemeContext.get_for_stage(global.stage);
        params = Params.parse(params, {
            file: null,
            cellWidth: null,
            cellHeight: null,
            frameWidth: null,
            frameHeight: null,
            speed: 0,
        });

        const {file, speed} = params;
        let {frameWidth, frameHeight, cellHeight, cellWidth} = params;

        if (!cellWidth)
            throw new Error('Animation has unspecified cell width');

        if (!frameHeight) {
            if (frameWidth)
                frameHeight = frameWidth;
            else
                frameHeight = cellHeight;
        }

        if (!frameWidth)
            frameWidth = cellWidth;

        if (!cellHeight)
            cellHeight = cellWidth;

        if (!frameHeight)
            frameHeight = cellWidth;

        super._init({
            style: `width: ${frameWidth}px; height: ${frameHeight}px;`,
        });

        this._file = file;
        this._cellWidth = cellWidth;
        this._cellHeight = cellHeight;
        this._frameWidth = frameWidth;
        this._frameHeight = frameHeight;

        this.connect('destroy', this._onDestroy.bind(this));
        this.connect('resource-scale-changed',
            () => this._loadFile());

        themeContext.connectObject('notify::scale-factor',
            () => {
                this._loadFile();
                this.set_size(
                    this._frameWidth * themeContext.scale_factor,
                    this._frameHeight * themeContext.scale_factor);
            }, this);

        this._speed = speed;

        this._isLoaded = false;
        this._isPlaying = false;
        this._timeoutId = 0;
        this._frame = 0;

        this._loadFile();
    }

    play() {
        if (this._isLoaded && this._timeoutId === 0) {
            if (this._frame === 0)
                this._showFrame(0);

            this._timeoutId = GLib.timeout_add(GLib.PRIORITY_LOW, this._speed, this._update.bind(this));
            GLib.Source.set_name_by_id(this._timeoutId, '[gnome-shell] this._update');
        }

        this._isPlaying = true;
    }

    stop() {
        if (this._timeoutId > 0) {
            GLib.source_remove(this._timeoutId);
            this._timeoutId = 0;
        }

        this._isPlaying = false;
    }

    _loadFile() {
        const resourceScale = this.get_resource_scale();
        let wasPlaying = this._isPlaying;

        if (this._isPlaying)
            this.stop();

        this._isLoaded = false;
        this.destroy_all_children();

        let textureCache = St.TextureCache.get_default();
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        this._animations = textureCache.load_sliced_image(this._file,
            this._cellWidth, this._cellHeight,
            this._frameWidth, this._frameHeight,
            scaleFactor, resourceScale,
            () => this._loadFinished());
        this._animations.set({
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });

        this.set_child(this._animations);

        if (wasPlaying)
            this.play();
    }

    _showFrame(frame) {
        let oldFrameActor = this._animations.get_child_at_index(this._frame);
        if (oldFrameActor)
            oldFrameActor.hide();

        this._frame = frame % this._animations.get_n_children();

        let newFrameActor = this._animations.get_child_at_index(this._frame);
        if (newFrameActor)
            newFrameActor.show();
    }

    _update() {
        this._showFrame(this._frame + 1);
        return GLib.SOURCE_CONTINUE;
    }

    _loadFinished() {
        this._isLoaded = this._animations.get_n_children() > 0;

        if (this._isLoaded && this._isPlaying)
            this.play();
    }

    _onDestroy() {
        this.stop();
    }
});

export const AnimatedIcon = GObject.registerClass(
class AnimatedIcon extends Animation {
    _init(params) {
        const {file, cellSize, frameSize} = params;

        super._init({
            file,
            cellWidth: cellSize,
            frameWidth: frameSize,
            speed: ANIMATED_ICON_UPDATE_TIMEOUT,
        });
    }
});

export const Spinner = GObject.registerClass(
class Spinner extends AnimatedIcon {
    _init(size, params) {
        params = Params.parse(params, {
            animate: false,
            hideOnStop: false,
        });
        this._fileDark = Gio.File.new_for_uri(
            'resource:///org/gnome/shell/theme/process-working-dark.svg');
        this._fileLight = Gio.File.new_for_uri(
            'resource:///org/gnome/shell/theme/process-working-light.svg');
        super._init({
            file: this._fileDark,
            cellSize: SPINNER_ANIMATION_CELL_SIZE,
            frameSize: size,
        });

        this.connect('style-changed', () => {
            const themeNode = this.get_theme_node();
            const textColor = themeNode.get_foreground_color();
            const [, luminance] = textColor.to_hls();
            const file = luminance > 0.5
                ? this._fileDark
                : this._fileLight;
            if (file !== this._file) {
                this._file = file;
                this._loadFile();
            }
        });

        this.opacity = 0;
        this._animate = params.animate;
        this._hideOnStop = params.hideOnStop;
        this.visible = !this._hideOnStop;
    }

    _onDestroy() {
        this._animate = false;
        super._onDestroy();
    }

    play() {
        this.remove_all_transitions();
        this.show();

        if (this._animate) {
            super.play();
            this.ease({
                opacity: 255,
                delay: SPINNER_ANIMATION_DELAY,
                duration: SPINNER_ANIMATION_TIME,
                mode: Clutter.AnimationMode.LINEAR,
            });
        } else {
            this.opacity = 255;
            super.play();
        }
    }

    stop() {
        this.remove_all_transitions();

        if (this._animate) {
            this.ease({
                opacity: 0,
                duration: SPINNER_ANIMATION_TIME,
                mode: Clutter.AnimationMode.LINEAR,
                onComplete: () => {
                    super.stop();
                    if (this._hideOnStop)
                        this.hide();
                },
            });
        } else {
            this.opacity = 0;
            super.stop();

            if (this._hideOnStop)
                this.hide();
        }
    }
});
(uuay)dnd.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';

import * as Main from './main.js';
import * as Params from '../misc/params.js';

// Time to scale down to maxDragActorSize
const SCALE_ANIMATION_TIME = 250;
// Time to animate to original position on cancel
const SNAP_BACK_ANIMATION_TIME = 250;
// Time to animate to original position on success
const REVERT_ANIMATION_TIME = 750;

/** @enum {number} */
export const DragMotionResult = {
    NO_DROP:   0,
    COPY_DROP: 1,
    MOVE_DROP: 2,
    CONTINUE:  3,
};

/** @enum {number} */
const DragState = {
    INIT:      0,
    DRAGGING:  1,
    CANCELLED: 2,
};

const DRAG_CURSOR_MAP = {
    0: Meta.Cursor.DND_UNSUPPORTED_TARGET,
    1: Meta.Cursor.DND_COPY,
    2: Meta.Cursor.DND_MOVE,
};

export const DragDropResult = {
    FAILURE:  0,
    SUCCESS:  1,
    CONTINUE: 2,
};
export const dragMonitors = [];

let eventHandlerActor = null;
let currentDraggable = null;

function _getEventHandlerActor() {
    if (!eventHandlerActor) {
        eventHandlerActor = new Clutter.Actor({width: 0, height: 0, reactive: true});
        Main.uiGroup.add_child(eventHandlerActor);
        // We connect to 'event' rather than 'captured-event' because the capturing phase doesn't happen
        // when you've grabbed the pointer.
        eventHandlerActor.connect('event', (actor, event) => {
            return currentDraggable._onEvent(actor, event);
        });
    }
    return eventHandlerActor;
}

function _getRealActorScale(actor) {
    let scale = 1.0;
    while (actor) {
        scale *= actor.scale_x;
        actor = actor.get_parent();
    }
    return scale;
}

/**
 * @typedef {object} DragMonitor
 * @property {Function} dragMotion
 */

/**
 * @param {DragMonitor} monitor
 */
export function addDragMonitor(monitor) {
    dragMonitors.push(monitor);
}

/**
 * @param {DragMonitor} monitor
 */
export function removeDragMonitor(monitor) {
    for (let i = 0; i < dragMonitors.length; i++) {
        if (dragMonitors[i] === monitor) {
            dragMonitors.splice(i, 1);
            return;
        }
    }
}

class _Draggable extends Signals.EventEmitter {
    constructor(actor, params) {
        super();

        params = Params.parse(params, {
            manualMode: false,
            timeoutThreshold: 0,
            restoreOnSuccess: false,
            dragActorMaxSize: undefined,
            dragActorOpacity: undefined,
        });

        this.actor = actor;
        this._dragState = DragState.INIT;

        if (!params.manualMode) {
            this.actor.connect('button-press-event',
                this._onButtonPress.bind(this));
            this.actor.connect('touch-event',
                this._onTouchEvent.bind(this));
        }

        this.actor.connect('destroy', () => {
            this._actorDestroyed = true;

            if (this._dragState === DragState.DRAGGING && this._dragCancellable)
                this._cancelDrag(global.get_current_time());
            this.disconnectAll();
        });
        this._onEventId = null;
        this._touchSequence = null;

        this._restoreOnSuccess = params.restoreOnSuccess;
        this._dragActorMaxSize = params.dragActorMaxSize;
        this._dragActorOpacity = params.dragActorOpacity;
        this._dragTimeoutThreshold = params.timeoutThreshold;

        this._animationInProgress = false; // The drag is over and the item is in the process of animating to its original position (snapping back or reverting).
        this._dragCancellable = true;
    }

    /**
     * addClickAction:
     *
     * @param {Clutter.ClickAction} action - click action to add to draggable actor
     *
     * Add @action to the draggable's actor, and set it up so that it does not
     * impede drag operations.
     */
    addClickAction(action) {
        action.connect('clicked', () => (this._actionClicked = true));
        action.connect('long-press', (a, actor, state) => {
            if (state !== Clutter.LongPressState.CANCEL)
                return true;

            const event = Clutter.get_current_event();
            this._dragTouchSequence = event.get_event_sequence();

            if (this._longPressLater)
                return true;

            // A click cancels a long-press before any click handler is
            // run - make sure to not start a drag in that case
            const laters = global.compositor.get_laters();
            this._longPressLater = laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                delete this._longPressLater;
                if (this._actionClicked) {
                    delete this._actionClicked;
                    return GLib.SOURCE_REMOVE;
                }
                action.release();
                this.startDrag(
                    ...action.get_coords(),
                    event.get_time(),
                    this._dragTouchSequence,
                    event.get_device());

                return GLib.SOURCE_REMOVE;
            });
            return true;
        });

        this.actor.add_action(action);
    }

    _onButtonPress(actor, event) {
        if (event.get_button() !== 1)
            return Clutter.EVENT_PROPAGATE;

        this._grabActor(event.get_device());

        let [stageX, stageY] = event.get_coords();
        this._dragStartX = stageX;
        this._dragStartY = stageY;
        this._dragStartTime = event.get_time();
        this._dragThresholdIgnored = false;

        return Clutter.EVENT_PROPAGATE;
    }

    _onTouchEvent(actor, event) {
        // We only handle touch events here on wayland. On X11
        // we do get emulated pointer events, which already works
        // for single-touch cases. Besides, the X11 passive touch grab
        // set up by Mutter will make us see first the touch events
        // and later the pointer events, so it will look like two
        // unrelated series of events, we want to avoid double handling
        // in these cases.
        if (!Meta.is_wayland_compositor())
            return Clutter.EVENT_PROPAGATE;

        if (event.type() !== Clutter.EventType.TOUCH_BEGIN ||
            !global.display.is_pointer_emulating_sequence(event.get_event_sequence()))
            return Clutter.EVENT_PROPAGATE;

        this._grabActor(event.get_device(), event.get_event_sequence());
        this._dragStartTime = event.get_time();
        this._dragThresholdIgnored = false;

        let [stageX, stageY] = event.get_coords();
        this._dragStartX = stageX;
        this._dragStartY = stageY;

        return Clutter.EVENT_PROPAGATE;
    }

    _grabDevice(actor, pointer, touchSequence) {
        this._grab = global.stage.grab(actor);
        this._grabbedDevice = pointer;
        this._touchSequence = touchSequence;
    }

    _ungrabDevice() {
        if (this._grab) {
            this._grab.dismiss();
            this._grab = null;
        }
        this._touchSequence = null;
        this._grabbedDevice = null;
    }

    _grabActor(device, touchSequence) {
        this._grabDevice(this.actor, device, touchSequence);
        this._onEventId = this.actor.connect('event',
            this._onEvent.bind(this));
    }

    _ungrabActor() {
        if (!this._onEventId)
            return;

        this._ungrabDevice();
        this.actor.disconnect(this._onEventId);
        this._onEventId = null;
    }

    _grabEvents(device, touchSequence) {
        if (!this._eventsGrab) {
            let grab = Main.pushModal(_getEventHandlerActor());
            if ((grab.get_seat_state() & Clutter.GrabState.POINTER) !== 0) {
                this._grabDevice(_getEventHandlerActor(), device, touchSequence);
                this._eventsGrab = grab;
            } else {
                Main.popModal(grab);
            }
        }
    }

    _ungrabEvents() {
        if (this._eventsGrab) {
            this._ungrabDevice();
            Main.popModal(this._eventsGrab);
            this._eventsGrab = null;
        }
    }

    _eventIsRelease(event) {
        if (event.type() === Clutter.EventType.BUTTON_RELEASE) {
            let buttonMask = Clutter.ModifierType.BUTTON1_MASK |
                              Clutter.ModifierType.BUTTON2_MASK |
                              Clutter.ModifierType.BUTTON3_MASK;
            /* We only obey the last button release from the device,
             * other buttons may get pressed/released during the DnD op.
             */
            return (event.get_state() & buttonMask) === 0;
        } else if (event.type() === Clutter.EventType.TOUCH_END) {
            /* For touch, we only obey the pointer emulating sequence */
            return global.display.is_pointer_emulating_sequence(event.get_event_sequence());
        }

        return false;
    }

    _onEvent(actor, event) {
        let device = event.get_device();

        if (this._grabbedDevice &&
            device !== this._grabbedDevice &&
            device.get_device_type() !== Clutter.InputDeviceType.KEYBOARD_DEVICE)
            return Clutter.EVENT_PROPAGATE;

        // We intercept BUTTON_RELEASE event to know that the button was released in case we
        // didn't start the drag, to drop the draggable in case the drag was in progress, and
        // to complete the drag and ensure that whatever happens to be under the pointer does
        // not get triggered if the drag was cancelled with Esc.
        if (this._eventIsRelease(event)) {
            if (this._dragState === DragState.DRAGGING) {
                return this._dragActorDropped(event);
            } else if ((this._dragActor != null || this._dragState === DragState.CANCELLED) &&
                       !this._animationInProgress) {
                // Drag must have been cancelled with Esc.
                this._dragComplete();
                return Clutter.EVENT_STOP;
            } else {
                // Drag has never started.
                this._ungrabActor();
                return Clutter.EVENT_PROPAGATE;
            }
        // We intercept MOTION event to figure out if the drag has started and to draw
        // this._dragActor under the pointer when dragging is in progress
        } else if (event.type() === Clutter.EventType.MOTION ||
                   (event.type() === Clutter.EventType.TOUCH_UPDATE &&
                    global.display.is_pointer_emulating_sequence(event.get_event_sequence()))) {
            if (this._dragActor && this._dragState === DragState.DRAGGING)
                return this._updateDragPosition(event);
            else if (this._dragActor == null && this._dragState !== DragState.CANCELLED)
                return this._maybeStartDrag(event);

        // We intercept KEY_PRESS event so that we can process Esc key press to cancel
        // dragging and ignore all other key presses.
        } else if (event.type() === Clutter.EventType.KEY_PRESS && this._dragState === DragState.DRAGGING) {
            let symbol = event.get_key_symbol();
            if (symbol === Clutter.KEY_Escape) {
                this._cancelDrag(event.get_time());
                return Clutter.EVENT_STOP;
            }
        }

        return Clutter.EVENT_PROPAGATE;
    }

    /**
     * Fake a release event.
     * Must be called if you want to intercept release events on draggable
     * actors for other purposes (for example if you're using
     * PopupMenu.ignoreRelease())
     */
    fakeRelease() {
        this._ungrabActor();
    }

    /**
     * Directly initiate a drag and drop operation from the given actor.
     * This function is useful to call if you've specified manualMode
     * for the draggable.
     *
     * @param {number} stageX - X coordinate of event
     * @param {number} stageY - Y coordinate of event
     * @param {number} time - Event timestamp
     * @param {Clutter.EventSequence=} sequence - Event sequence
     * @param {Clutter.InputDevice=} device - device that originated the event
     */
    startDrag(stageX, stageY, time, sequence, device) {
        if (currentDraggable)
            return;

        if (device === undefined) {
            let event = Clutter.get_current_event();

            if (event)
                device = event.get_device();

            if (device === undefined) {
                let seat = Clutter.get_default_backend().get_default_seat();
                device = seat.get_pointer();
            }
        }

        currentDraggable = this;
        this._dragState = DragState.DRAGGING;

        // Special-case St.Button: the pointer grab messes with the internal
        // state, so force a reset to a reasonable state here
        if (this.actor instanceof St.Button) {
            this.actor.fake_release();
            this.actor.hover = false;
        }

        this.emit('drag-begin', time);
        if (this._onEventId)
            this._ungrabActor();

        this._grabEvents(device, sequence);
        global.display.set_cursor(Meta.Cursor.DND_IN_DRAG);

        this._dragX = this._dragStartX = stageX;
        this._dragY = this._dragStartY = stageY;

        let scaledWidth, scaledHeight;

        if (this.actor._delegate && this.actor._delegate.getDragActor) {
            this._dragActor = this.actor._delegate.getDragActor();
            Main.uiGroup.add_child(this._dragActor);
            Main.uiGroup.set_child_above_sibling(this._dragActor, null);
            Shell.util_set_hidden_from_pick(this._dragActor, true);

            // Drag actor does not always have to be the same as actor. For example drag actor
            // can be an image that's part of the actor. So to perform "snap back" correctly we need
            // to know what was the drag actor source.
            if (this.actor._delegate.getDragActorSource) {
                this._dragActorSource = this.actor._delegate.getDragActorSource();
                // If the user dragged from the source, then position
                // the dragActor over it. Otherwise, center it
                // around the pointer
                let [sourceX, sourceY] = this._dragActorSource.get_transformed_position();
                let x, y;
                if (stageX > sourceX && stageX <= sourceX + this._dragActor.width &&
                    stageY > sourceY && stageY <= sourceY + this._dragActor.height) {
                    x = sourceX;
                    y = sourceY;
                } else {
                    x = stageX - this._dragActor.width / 2;
                    y = stageY - this._dragActor.height / 2;
                }
                this._dragActor.set_position(x, y);

                this._dragActorSourceDestroyId = this._dragActorSource.connect('destroy', () => {
                    this._dragActorSource = null;
                });
            } else {
                this._dragActorSource = this.actor;
            }
            this._dragOrigParent = undefined;

            this._dragOffsetX = this._dragActor.x - this._dragStartX;
            this._dragOffsetY = this._dragActor.y - this._dragStartY;

            [scaledWidth, scaledHeight] = this._dragActor.get_transformed_size();
        } else {
            this._dragActor = this.actor;

            this._dragActorSource = undefined;
            this._dragOrigParent = this.actor.get_parent();
            this._dragActorHadFixedPos = this._dragActor.fixed_position_set;
            this._dragOrigX = this._dragActor.allocation.x1;
            this._dragOrigY = this._dragActor.allocation.y1;
            this._dragActorHadNatWidth = this._dragActor.natural_width_set;
            this._dragActorHadNatHeight = this._dragActor.natural_height_set;
            this._dragOrigWidth = this._dragActor.allocation.get_width();
            this._dragOrigHeight = this._dragActor.allocation.get_height();
            this._dragOrigScale = this._dragActor.scale_x;

            // Ensure actors with an allocation smaller than their natural size
            // retain their size
            this._dragActor.set_size(...this._dragActor.allocation.get_size());

            const transformedExtents = this._dragActor.get_transformed_extents();

            this._dragOffsetX = transformedExtents.origin.x - this._dragStartX;
            this._dragOffsetY = transformedExtents.origin.y - this._dragStartY;

            scaledWidth = transformedExtents.get_width();
            scaledHeight = transformedExtents.get_height();

            this._dragActor.scale_x = scaledWidth / this._dragOrigWidth;
            this._dragActor.scale_y = scaledHeight / this._dragOrigHeight;

            this._dragOrigParent.remove_child(this._dragActor);
            Main.uiGroup.add_child(this._dragActor);
            Main.uiGroup.set_child_above_sibling(this._dragActor, null);
            Shell.util_set_hidden_from_pick(this._dragActor, true);

            this._dragOrigParentDestroyId = this._dragOrigParent.connect('destroy', () => {
                this._dragOrigParent = null;
            });
        }

        this._dragActorDestroyId = this._dragActor.connect('destroy', () => {
            // Cancel ongoing animation (if any)
            this._finishAnimation();

            this._dragActor = null;
            if (this._dragState === DragState.DRAGGING)
                this._dragState = DragState.CANCELLED;
        });
        this._dragOrigOpacity = this._dragActor.opacity;
        if (this._dragActorOpacity !== undefined)
            this._dragActor.opacity = this._dragActorOpacity;

        this._snapBackX = this._dragStartX + this._dragOffsetX;
        this._snapBackY = this._dragStartY + this._dragOffsetY;
        this._snapBackScale = this._dragActor.scale_x;

        let origDragOffsetX = this._dragOffsetX;
        let origDragOffsetY = this._dragOffsetY;
        let [transX, transY] = this._dragActor.get_translation();
        this._dragOffsetX -= transX;
        this._dragOffsetY -= transY;

        this._dragActor.set_position(
            this._dragX + this._dragOffsetX,
            this._dragY + this._dragOffsetY);

        if (this._dragActorMaxSize !== undefined) {
            let currentSize = Math.max(scaledWidth, scaledHeight);
            if (currentSize > this._dragActorMaxSize) {
                let scale = this._dragActorMaxSize / currentSize;
                let origScale =  this._dragActor.scale_x;

                // The position of the actor changes as we scale
                // around the drag position, but we can't just tween
                // to the final position because that tween would
                // fight with updates as the user continues dragging
                // the mouse; instead we do the position computations in
                // a ::new-frame handler.
                this._dragActor.ease({
                    scale_x: scale * origScale,
                    scale_y: scale * origScale,
                    duration: SCALE_ANIMATION_TIME,
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                    onComplete: () => {
                        this._updateActorPosition(origScale,
                            origDragOffsetX, origDragOffsetY, transX, transY);
                    },
                });

                this._dragActor.get_transition('scale-x').connect('new-frame', () => {
                    this._updateActorPosition(origScale,
                        origDragOffsetX, origDragOffsetY, transX, transY);
                });
            }
        }
    }

    _updateActorPosition(origScale, origDragOffsetX, origDragOffsetY, transX, transY) {
        const currentScale = this._dragActor.scale_x / origScale;
        this._dragOffsetX = currentScale * origDragOffsetX - transX;
        this._dragOffsetY = currentScale * origDragOffsetY - transY;
        this._dragActor.set_position(
            this._dragX + this._dragOffsetX,
            this._dragY + this._dragOffsetY);
    }

    _maybeStartDrag(event) {
        let [stageX, stageY] = event.get_coords();

        if (this._dragThresholdIgnored)
            return Clutter.EVENT_PROPAGATE;

        // See if the user has moved the mouse enough to trigger a drag
        let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
        let threshold = St.Settings.get().drag_threshold * scaleFactor;
        if (!currentDraggable &&
            (Math.abs(stageX - this._dragStartX) > threshold ||
             Math.abs(stageY - this._dragStartY) > threshold)) {
            const deviceType = event.get_source_device().get_device_type();
            const isPointerOrTouchpad =
                deviceType === Clutter.InputDeviceType.POINTER_DEVICE ||
                deviceType === Clutter.InputDeviceType.TOUCHPAD_DEVICE;
            const ellapsedTime = event.get_time() - this._dragStartTime;

            // Pointer devices (e.g. mouse) start the drag immediately
            if (isPointerOrTouchpad || ellapsedTime > this._dragTimeoutThreshold) {
                this.startDrag(stageX, stageY, event.get_time(), this._touchSequence, event.get_device());
                this._updateDragPosition(event);
            } else {
                this._dragThresholdIgnored = true;
                this._ungrabActor();
                return Clutter.EVENT_PROPAGATE;
            }
        }

        return Clutter.EVENT_STOP;
    }

    _pickTargetActor() {
        return this._dragActor.get_stage().get_actor_at_pos(
            Clutter.PickMode.ALL, this._dragX, this._dragY);
    }

    _updateDragHover() {
        this._updateHoverId = 0;
        let target = this._pickTargetActor();

        let dragEvent = {
            x: this._dragX,
            y: this._dragY,
            dragActor: this._dragActor,
            source: this.actor._delegate,
            targetActor: target,
        };

        let targetActorDestroyHandlerId;
        let handleTargetActorDestroyClosure;
        handleTargetActorDestroyClosure = () => {
            target = this._pickTargetActor();
            dragEvent.targetActor = target;
            targetActorDestroyHandlerId =
                target.connect('destroy', handleTargetActorDestroyClosure);
        };
        targetActorDestroyHandlerId =
            target.connect('destroy', handleTargetActorDestroyClosure);

        for (let i = 0; i < dragMonitors.length; i++) {
            let motionFunc = dragMonitors[i].dragMotion;
            if (motionFunc) {
                let result = motionFunc(dragEvent);
                if (result !== DragMotionResult.CONTINUE) {
                    global.display.set_cursor(DRAG_CURSOR_MAP[result]);
                    dragEvent.targetActor.disconnect(targetActorDestroyHandlerId);
                    return GLib.SOURCE_REMOVE;
                }
            }
        }
        dragEvent.targetActor.disconnect(targetActorDestroyHandlerId);

        while (target) {
            if (target._delegate && target._delegate.handleDragOver) {
                let [r_, targX, targY] = target.transform_stage_point(this._dragX, this._dragY);
                // We currently loop through all parents on drag-over even if one of the children has handled it.
                // We can check the return value of the function and break the loop if it's true if we don't want
                // to continue checking the parents.
                let result = target._delegate.handleDragOver(
                    this.actor._delegate,
                    this._dragActor,
                    targX,
                    targY,
                    0);
                if (result !== DragMotionResult.CONTINUE) {
                    global.display.set_cursor(DRAG_CURSOR_MAP[result]);
                    return GLib.SOURCE_REMOVE;
                }
            }
            target = target.get_parent();
        }
        global.display.set_cursor(Meta.Cursor.DND_IN_DRAG);
        return GLib.SOURCE_REMOVE;
    }

    _queueUpdateDragHover() {
        if (this._updateHoverId)
            return;

        this._updateHoverId = GLib.idle_add(GLib.PRIORITY_DEFAULT,
            this._updateDragHover.bind(this));
        GLib.Source.set_name_by_id(this._updateHoverId, '[gnome-shell] this._updateDragHover');
    }

    _updateDragPosition(event) {
        let [stageX, stageY] = event.get_coords();
        this._dragX = stageX;
        this._dragY = stageY;
        this._dragActor.set_position(
            stageX + this._dragOffsetX,
            stageY + this._dragOffsetY);

        this._queueUpdateDragHover();
        return true;
    }

    _dragActorDropped(event) {
        let [dropX, dropY] = event.get_coords();
        let target = this._dragActor.get_stage().get_actor_at_pos(
            Clutter.PickMode.ALL, dropX, dropY);

        // We call observers only once per motion with the innermost
        // target actor. If necessary, the observer can walk the
        // parent itself.
        let dropEvent = {
            dropActor: this._dragActor,
            targetActor: target,
            clutterEvent: event,
        };
        for (let i = 0; i < dragMonitors.length; i++) {
            let dropFunc = dragMonitors[i].dragDrop;
            if (dropFunc) {
                switch (dropFunc(dropEvent)) {
                case DragDropResult.FAILURE:
                case DragDropResult.SUCCESS:
                    return true;
                case DragDropResult.CONTINUE:
                    continue;
                }
            }
        }

        // At this point it is too late to cancel a drag by destroying
        // the actor, the fate of which is decided by acceptDrop and its
        // side-effects
        this._dragCancellable = false;

        while (target) {
            if (target._delegate && target._delegate.acceptDrop) {
                let [r_, targX, targY] = target.transform_stage_point(dropX, dropY);
                let accepted = false;
                try {
                    accepted = target._delegate.acceptDrop(this.actor._delegate,
                        this._dragActor, targX, targY, event.get_time());
                } catch (e) {
                    // On error, skip this target
                    logError(e, 'Skipping drag target');
                }
                if (accepted) {
                    // If it accepted the drop without taking the actor,
                    // handle it ourselves.
                    if (this._dragActor && this._dragActor.get_parent() === Main.uiGroup) {
                        if (this._restoreOnSuccess) {
                            this._restoreDragActor(event.get_time());
                            return true;
                        } else {
                            this._dragActor.destroy();
                        }
                    }

                    this._dragState = DragState.INIT;
                    global.display.set_cursor(Meta.Cursor.DEFAULT);
                    this.emit('drag-end', event.get_time(), true);
                    this._dragComplete();
                    return true;
                }
            }
            target = target.get_parent();
        }

        this._cancelDrag(event.get_time());

        return true;
    }

    _getRestoreLocation() {
        let x, y, scale;

        if (this._dragActorSource && this._dragActorSource.visible) {
            // Snap the clone back to its source
            [x, y] = this._dragActorSource.get_transformed_position();
            let [sourceScaledWidth] = this._dragActorSource.get_transformed_size();
            scale = sourceScaledWidth ? sourceScaledWidth / this._dragActor.width : 0;
        } else if (this._dragOrigParent) {
            // Snap the actor back to its original position within
            // its parent, adjusting for the fact that the parent
            // may have been moved or scaled
            let [parentX, parentY] = this._dragOrigParent.get_transformed_position();
            let parentScale = _getRealActorScale(this._dragOrigParent);

            x = parentX + parentScale * this._dragOrigX;
            y = parentY + parentScale * this._dragOrigY;
            scale = this._dragOrigScale * parentScale;
        } else {
            // Snap back actor to its original stage position
            x = this._snapBackX;
            y = this._snapBackY;
            scale = this._snapBackScale;
        }

        return [x, y, scale];
    }

    _cancelDrag(eventTime) {
        this.emit('drag-cancelled', eventTime);
        let wasCancelled = this._dragState === DragState.CANCELLED;
        this._dragState = DragState.CANCELLED;

        if (this._actorDestroyed || wasCancelled) {
            global.display.set_cursor(Meta.Cursor.DEFAULT);
            this._dragComplete();
            this.emit('drag-end', eventTime, false);
            if (!this._dragOrigParent && this._dragActor)
                this._dragActor.destroy();

            return;
        }

        let [snapBackX, snapBackY, snapBackScale] = this._getRestoreLocation();

        this._animateDragEnd(eventTime, {
            x: snapBackX,
            y: snapBackY,
            scale_x: snapBackScale,
            scale_y: snapBackScale,
            duration: SNAP_BACK_ANIMATION_TIME,
        });
    }

    _restoreDragActor(eventTime) {
        this._dragState = DragState.INIT;
        let [restoreX, restoreY, restoreScale] = this._getRestoreLocation();

        // fade the actor back in at its original location
        this._dragActor.set_position(restoreX, restoreY);
        this._dragActor.set_scale(restoreScale, restoreScale);
        this._dragActor.opacity = 0;

        this._animateDragEnd(eventTime, {
            duration: REVERT_ANIMATION_TIME,
        });
    }

    _animateDragEnd(eventTime, params) {
        this._animationInProgress = true;

        // start the animation
        this._dragActor.ease({
            ...params,
            opacity: this._dragOrigOpacity,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this._onAnimationComplete(this._dragActor, eventTime);
            },
        });
    }

    _finishAnimation() {
        if (!this._animationInProgress)
            return;

        this._animationInProgress = false;
        this._dragComplete();

        global.display.set_cursor(Meta.Cursor.DEFAULT);
    }

    _onAnimationComplete(dragActor, eventTime) {
        if (this._dragOrigParent) {
            Main.uiGroup.remove_child(this._dragActor);
            this._dragOrigParent.add_child(this._dragActor);
            dragActor.set_scale(this._dragOrigScale, this._dragOrigScale);
            if (this._dragActorHadFixedPos)
                dragActor.set_position(this._dragOrigX, this._dragOrigY);
            else
                dragActor.fixed_position_set = false;
            if (this._dragActorHadNatWidth)
                this._dragActor.set_width(-1);
            if (this._dragActorHadNatHeight)
                this._dragActor.set_height(-1);
        } else {
            dragActor.destroy();
        }

        this.emit('drag-end', eventTime, false);
        this._finishAnimation();
    }

    _dragComplete() {
        if (!this._actorDestroyed && this._dragActor)
            Shell.util_set_hidden_from_pick(this._dragActor, false);

        this._ungrabEvents();

        if (this._updateHoverId) {
            GLib.source_remove(this._updateHoverId);
            this._updateHoverId = 0;
        }

        if (this._dragActor) {
            this._dragActor.disconnect(this._dragActorDestroyId);
            this._dragActor = null;
        }

        if (this._dragOrigParent) {
            this._dragOrigParent.disconnect(this._dragOrigParentDestroyId);
            this._dragOrigParent = null;
        }

        if (this._dragActorSource) {
            this._dragActorSource.disconnect(this._dragActorSourceDestroyId);
            this._dragActorSource = null;
        }

        this._dragState = DragState.INIT;
        currentDraggable = null;
    }
}

/**
 * Create an object which controls drag and drop for the given actor.
 *
 * If %manualMode is %true in @params, do not automatically start
 * drag and drop on click
 *
 * If %dragActorMaxSize is present in @params, the drag actor will
 * be scaled down to be no larger than that size in pixels.
 *
 * If %dragActorOpacity is present in @params, the drag actor will
 * will be set to have that opacity during the drag.
 *
 * Note that when the drag actor is the source actor and the drop
 * succeeds, the actor scale and opacity aren't reset; if the drop
 * target wants to reuse the actor, it's up to the drop target to
 * reset these values.
 *
 * @param {Clutter.Actor} actor Source actor
 * @param {object} [params] Additional parameters
 * @returns {_Draggable} a new Draggable
 */
export function makeDraggable(actor, params) {
    return new _Draggable(actor, params);
}
(uuay)modemManager.js�'// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import NM from 'gi://NM';
import NMA4 from 'gi://NMA4';

import {loadInterfaceXML} from './fileUtils.js';

let _mpd;

/**
 * _getMobileProvidersDatabase:
 *
 * Gets the database of mobile providers, with references between MCCMNC/SID and
 * operator name
 *
 * @returns {NMA4.MobileProvidersDatabase | null}
 */
function _getMobileProvidersDatabase() {
    if (_mpd == null) {
        try {
            _mpd = new NMA4.MobileProvidersDatabase();
            _mpd.init(null);
        } catch (e) {
            log(e.message);
            _mpd = null;
        }
    }

    return _mpd;
}

// _findProviderForMccMnc:
// @operatorName: operator name
// @operatorCode: operator code
//
// Given an operator name string (which may not be a real operator name) and an
// operator code string, tries to find a proper operator name to display.
//
function _findProviderForMccMnc(operatorName, operatorCode) {
    if (operatorName) {
        if (operatorName.length !== 0 &&
            (operatorName.length > 6 || operatorName.length < 5)) {
            // this looks like a valid name, i.e. not an MCCMNC (that some
            // devices return when not yet connected
            return operatorName;
        }

        if (isNaN(parseInt(operatorName))) {
            // name is definitely not a MCCMNC, so it may be a name
            // after all; return that
            return operatorName;
        }
    }

    let needle;
    if ((!operatorName || operatorName.length === 0) && operatorCode)
        needle = operatorCode;
    else if (operatorName && (operatorName.length === 6 || operatorName.length === 5))
        needle = operatorName;
    else // nothing to search
        return null;

    let mpd = _getMobileProvidersDatabase();
    if (mpd) {
        let provider = mpd.lookup_3gpp_mcc_mnc(needle);
        if (provider)
            return provider.get_name();
    }
    return null;
}

// _findProviderForSid:
// @sid: System Identifier of the serving CDMA network
//
// Tries to find the operator name corresponding to the given SID
//
function _findProviderForSid(sid) {
    if (!sid)
        return null;

    let mpd = _getMobileProvidersDatabase();
    if (mpd) {
        let provider = mpd.lookup_cdma_sid(sid);
        if (provider)
            return provider.get_name();
    }
    return null;
}


// ----------------------------------------------------- //
// Support for the old ModemManager interface (MM < 0.7) //
// ----------------------------------------------------- //


// The following are not the complete interfaces, just the methods we need
// (or may need in the future)

const ModemGsmNetworkInterface = loadInterfaceXML('org.freedesktop.ModemManager.Modem.Gsm.Network');
const ModemGsmNetworkProxy = Gio.DBusProxy.makeProxyWrapper(ModemGsmNetworkInterface);

const ModemCdmaInterface = loadInterfaceXML('org.freedesktop.ModemManager.Modem.Cdma');
const ModemCdmaProxy = Gio.DBusProxy.makeProxyWrapper(ModemCdmaInterface);

const ModemBase = GObject.registerClass({
    GTypeFlags: GObject.TypeFlags.ABSTRACT,
    Properties: {
        'operator-name': GObject.ParamSpec.string(
            'operator-name', 'operator-name', 'operator-name',
            GObject.ParamFlags.READABLE,
            null),
        'signal-quality': GObject.ParamSpec.int(
            'signal-quality', 'signal-quality', 'signal-quality',
            GObject.ParamFlags.READABLE,
            0, 100, 0),
    },
}, class ModemBase extends GObject.Object {
    _init() {
        super._init();
        this._operatorName = null;
        this._signalQuality = 0;
    }

    get operatorName() {
        return this._operatorName;
    }

    get signalQuality() {
        return this._signalQuality;
    }

    _setOperatorName(operatorName) {
        if (this._operatorName === operatorName)
            return;
        this._operatorName = operatorName;
        this.notify('operator-name');
    }

    _setSignalQuality(signalQuality) {
        if (this._signalQuality === signalQuality)
            return;
        this._signalQuality = signalQuality;
        this.notify('signal-quality');
    }
});

export const ModemGsm = GObject.registerClass(
class ModemGsm extends ModemBase {
    _init(path) {
        super._init();
        this._proxy = new ModemGsmNetworkProxy(Gio.DBus.system, 'org.freedesktop.ModemManager', path);

        // Code is duplicated because the function have different signatures
        this._proxy.connectSignal('SignalQuality', (proxy, sender, [quality]) => {
            this._setSignalQuality(quality);
        });
        this._proxy.connectSignal('RegistrationInfo', (proxy, sender, [_status, code, name]) => {
            this._setOperatorName(_findProviderForMccMnc(name, code));
        });
        this._getInitialState();
    }

    async _getInitialState() {
        try {
            const [
                [status_, code, name],
                [quality],
            ] = await Promise.all([
                this._proxy.GetRegistrationInfoAsync(),
                this._proxy.GetSignalQualityAsync(),
            ]);
            this._setOperatorName(_findProviderForMccMnc(name, code));
            this._setSignalQuality(quality);
        } catch (err) {
            // it will return an error if the device is not connected
            this._setSignalQuality(0);
        }
    }
});

export const ModemCdma = GObject.registerClass(
class ModemCdma extends ModemBase {
    _init(path) {
        super._init();
        this._proxy = new ModemCdmaProxy(Gio.DBus.system, 'org.freedesktop.ModemManager', path);

        this._proxy.connectSignal('SignalQuality', (proxy, sender, params) => {
            this._setSignalQuality(params[0]);

            // receiving this signal means the device got activated
            // and we can finally call GetServingSystem
            if (this.operator_name == null)
                this._refreshServingSystem();
        });
        this._getSignalQuality();
    }

    async _getSignalQuality() {
        try {
            const [quality] = await this._proxy.GetSignalQualityAsync();
            this._setSignalQuality(quality);
        } catch (err) {
            // it will return an error if the device is not connected
            this._setSignalQuality(0);
        }
    }

    async _refreshServingSystem() {
        try {
            const [bandClass_, band_, sid] =
                await this._proxy.GetServingSystemAsync();
            this._setOperatorName(_findProviderForSid(sid));
        } catch (err) {
            // it will return an error if the device is not connected
            this._setOperatorName(null);
        }
    }
});


// ------------------------------------------------------- //
// Support for the new ModemManager1 interface (MM >= 0.7) //
// ------------------------------------------------------- //

const BroadbandModemInterface = loadInterfaceXML('org.freedesktop.ModemManager1.Modem');
const BroadbandModemProxy = Gio.DBusProxy.makeProxyWrapper(BroadbandModemInterface);

const BroadbandModem3gppInterface = loadInterfaceXML('org.freedesktop.ModemManager1.Modem.Modem3gpp');
const BroadbandModem3gppProxy = Gio.DBusProxy.makeProxyWrapper(BroadbandModem3gppInterface);

const BroadbandModemCdmaInterface = loadInterfaceXML('org.freedesktop.ModemManager1.Modem.ModemCdma');
const BroadbandModemCdmaProxy = Gio.DBusProxy.makeProxyWrapper(BroadbandModemCdmaInterface);

export const BroadbandModem = GObject.registerClass({
    Properties: {
        'capabilities': GObject.ParamSpec.flags(
            'capabilities', 'capabilities', 'capabilities',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            NM.DeviceModemCapabilities.$gtype,
            NM.DeviceModemCapabilities.NONE),
    },
}, class BroadbandModem extends ModemBase {
    _init(path, capabilities) {
        super._init({capabilities});
        this._proxy = new BroadbandModemProxy(Gio.DBus.system, 'org.freedesktop.ModemManager1', path);
        this._proxy_3gpp = new BroadbandModem3gppProxy(Gio.DBus.system, 'org.freedesktop.ModemManager1', path);
        this._proxy_cdma = new BroadbandModemCdmaProxy(Gio.DBus.system, 'org.freedesktop.ModemManager1', path);

        this._proxy.connect('g-properties-changed', (proxy, properties) => {
            const signalQualityChanged = !!properties.lookup_value('SignalQuality', null);
            if (signalQualityChanged)
                this._reloadSignalQuality();
        });
        this._reloadSignalQuality();

        this._proxy_3gpp.connect('g-properties-changed', (proxy, properties) => {
            let unpacked = properties.deepUnpack();
            if ('OperatorName' in unpacked || 'OperatorCode' in unpacked)
                this._reload3gppOperatorName();
        });
        this._reload3gppOperatorName();

        this._proxy_cdma.connect('g-properties-changed', (proxy, properties) => {
            let unpacked = properties.deepUnpack();
            if ('Nid' in unpacked || 'Sid' in unpacked)
                this._reloadCdmaOperatorName();
        });
        this._reloadCdmaOperatorName();
    }

    _reloadSignalQuality() {
        let [quality, recent_] = this._proxy.SignalQuality;
        this._setSignalQuality(quality);
    }

    _reloadOperatorName() {
        let newName = '';
        if (this.operator_name_3gpp && this.operator_name_3gpp.length > 0)
            newName += this.operator_name_3gpp;

        if (this.operator_name_cdma && this.operator_name_cdma.length > 0) {
            if (newName !== '')
                newName += ', ';
            newName += this.operator_name_cdma;
        }

        this._setOperatorName(newName);
    }

    _reload3gppOperatorName() {
        let name = this._proxy_3gpp.OperatorName;
        let code = this._proxy_3gpp.OperatorCode;
        this.operator_name_3gpp = _findProviderForMccMnc(name, code);
        this._reloadOperatorName();
    }

    _reloadCdmaOperatorName() {
        let sid = this._proxy_cdma.Sid;
        this.operator_name_cdma = _findProviderForSid(sid);
        this._reloadOperatorName();
    }
});
(uuay)backlight.js.// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported Indicator */

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import St from 'gi://St';

import {QuickMenuToggle, SystemIndicator} from '../quickSettings.js';

import * as PopupMenu from '../popupMenu.js';
import {Slider} from '../slider.js';

import {loadInterfaceXML} from '../../misc/fileUtils.js';

const BUS_NAME = 'org.gnome.SettingsDaemon.Power';
const OBJECT_PATH = '/org/gnome/SettingsDaemon/Power';

const BrightnessInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Power.Keyboard');
const BrightnessProxy = Gio.DBusProxy.makeProxyWrapper(BrightnessInterface);

const SliderItem = GObject.registerClass({
    Properties: {
        'value': GObject.ParamSpec.int(
            'value', '', '',
            GObject.ParamFlags.READWRITE,
            0, 100, 0),
    },
}, class SliderItem extends PopupMenu.PopupBaseMenuItem {
    constructor() {
        super({
            activate: false,
            style_class: 'keyboard-brightness-item',
        });

        this._slider = new Slider(0);

        this._sliderChangedId = this._slider.connect('notify::value',
            () => this.notify('value'));
        this._slider.accessible_name = _('Keyboard Brightness');

        this.add_child(this._slider);
    }

    get value() {
        return this._slider.value * 100;
    }

    set value(value) {
        if (this.value === value)
            return;

        this._slider.block_signal_handler(this._sliderChangedId);
        this._slider.value = value / 100;
        this._slider.unblock_signal_handler(this._sliderChangedId);

        this.notify('value');
    }
});

const DiscreteItem = GObject.registerClass({
    Properties: {
        'value': GObject.ParamSpec.int(
            'value', '', '',
            GObject.ParamFlags.READWRITE,
            0, 100, 0),
        'n-levels': GObject.ParamSpec.int(
            'n-levels', '', '',
            GObject.ParamFlags.READWRITE,
            1, 3, 1),
    },
}, class DiscreteItem extends St.BoxLayout {
    constructor() {
        super({
            style_class: 'popup-menu-item',
            reactive: true,
        });

        this._levelButtons = new Map();
        this._addLevelButton('off', _('Off'), 'keyboard-brightness-off-symbolic');
        this._addLevelButton('low', _('Low'), 'keyboard-brightness-medium-symbolic');
        this._addLevelButton('high', _('High'), 'keyboard-brightness-high-symbolic');

        this.connect('notify::n-levels', () => this._syncLevels());
        this.connect('notify::value', () => this._syncChecked());
        this._syncLevels();
    }

    _valueToLevel(value) {
        const checkedIndex = Math.round(value * (this.nLevels - 1) / 100);
        if (checkedIndex === this.nLevels - 1)
            return 'high';

        return [...this._levelButtons.keys()].at(checkedIndex);
    }

    _levelToValue(level) {
        const keyIndex = [...this._levelButtons.keys()].indexOf(level);
        return 100 * Math.min(keyIndex, this.nLevels - 1) / (this.nLevels - 1);
    }

    _addLevelButton(key, label, iconName) {
        const box = new St.BoxLayout({
            style_class: 'keyboard-brightness-level',
            vertical: true,
            x_expand: true,
        });

        box.button = new St.Button({
            styleClass: 'icon-button',
            canFocus: true,
            iconName,
        });
        box.add_child(box.button);

        box.button.connect('clicked', () => {
            this.value = this._levelToValue(key);
        });

        box.add_child(new St.Label({
            text: label,
            x_align: Clutter.ActorAlign.CENTER,
        }));

        this.add_child(box);
        this._levelButtons.set(key, box);
    }

    _syncLevels() {
        this._levelButtons.get('off').visible = this.nLevels > 0;
        this._levelButtons.get('high').visible = this.nLevels > 1;
        this._levelButtons.get('low').visible = this.nLevels > 2;
        this._syncChecked();
    }

    _syncChecked() {
        const checkedKey = this._valueToLevel(this.value);
        this._levelButtons.forEach((b, k) => {
            b.button.checked = k === checkedKey;
        });
    }
});

const KeyboardBrightnessToggle = GObject.registerClass(
class KeyboardBrightnessToggle extends QuickMenuToggle {
    _init() {
        super._init({
            title: _('Keyboard'),
            iconName: 'display-brightness-symbolic',
        });

        this._proxy = new BrightnessProxy(Gio.DBus.session, BUS_NAME, OBJECT_PATH,
            (proxy, error) => {
                if (error)
                    console.error(error.message);
                else
                    this._proxy.connect('g-properties-changed', () => this._sync());
                this._sync();
            });

        this.connect('clicked', () => {
            this._proxy.Brightness = this.checked ? 0 : 100;
        });

        this._sliderItem = new SliderItem();
        this.menu.box.add_child(this._sliderItem);

        this._discreteItem = new DiscreteItem();
        this.menu.box.add_child(this._discreteItem);

        this._sliderItem.bind_property('visible',
            this._discreteItem, 'visible',
            GObject.BindingFlags.INVERT_BOOLEAN |
            GObject.BindingFlags.SYNC_CREATE);

        this._sliderItem.bind_property('value',
            this._discreteItem, 'value',
            GObject.BindingFlags.SYNC_CREATE);

        this._sliderItemChangedId = this._sliderItem.connect('notify::value', () => {
            if (this._sliderItem.visible)
                this._proxy.Brightness = this._sliderItem.value;
        });

        this._discreteItemChangedId = this._discreteItem.connect('notify::value', () => {
            if (this._discreteItem.visible)
                this._proxy.Brightness = this._discreteItem.value;
        });
    }

    _sync() {
        const brightness = this._proxy.Brightness;
        const visible = Number.isInteger(brightness) && brightness >= 0;
        this.visible = visible;
        if (!visible)
            return;

        this.checked = brightness > 0;
        const useSlider = this._proxy.Steps >= 4;

        this._sliderItem.block_signal_handler(this._sliderItemChangedId);
        this._discreteItem.block_signal_handler(this._discreteItemChangedId);

        this._sliderItem.set({
            visible: useSlider,
            value: brightness,
        });

        if (!useSlider)
            this._discreteItem.nLevels = this._proxy.Steps;

        this._sliderItem.unblock_signal_handler(this._sliderItemChangedId);
        this._discreteItem.unblock_signal_handler(this._discreteItemChangedId);
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this.quickSettingsItems.push(new KeyboardBrightnessToggle());
    }
});
(uuay)util.js�5// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Shell from 'gi://Shell';
import St from 'gi://St';
import GnomeDesktop from 'gi://GnomeDesktop';

import * as Main from '../ui/main.js';
import {formatTime} from './dateUtils.js';

// http://daringfireball.net/2010/07/improved_regex_for_matching_urls
const _balancedParens = '\\([^\\s()<>]+\\)';
const _leadingJunk = '[\\s`(\\[{\'\\"<\u00AB\u201C\u2018]';
const _notTrailingJunk = '[^\\s`!()\\[\\]{};:\'\\".,<>?\u00AB\u00BB\u200E\u200F\u201C\u201D\u2018\u2019\u202A\u202C]';

const _urlRegexp = new RegExp(
    `(^|${_leadingJunk})` +
    '(' +
        '(?:' +
            '(?:http|https|ftp)://' +             // scheme://
            '|' +
            'www\\d{0,3}[.]' +                    // www.
            '|' +
            '[a-z0-9.\\-]+[.][a-z]{2,4}/' +       // foo.xx/
        ')' +
        '(?:' +                                   // one or more:
            '[^\\s()<>]+' +                       // run of non-space non-()
            '|' +                                 // or
            `${_balancedParens}` +                // balanced parens
        ')+' +
        '(?:' +                                   // end with:
            `${_balancedParens}` +                // balanced parens
            '|' +                                 // or
            `${_notTrailingJunk}` +               // last non-junk char
        ')' +
    ')', 'gi');

let _desktopSettings = null;

/**
 * findUrls:
 *
 * @param {string} str string to find URLs in
 *
 * Searches `str` for URLs and returns an array of objects with %url
 * properties showing the matched URL string, and %pos properties indicating
 * the position within `str` where the URL was found.
 *
 * @returns {{url: string, pos: number}[]} the list of match objects, as described above
 */
export function findUrls(str) {
    let res = [], match;
    while ((match = _urlRegexp.exec(str)))
        res.push({url: match[2], pos: match.index + match[1].length});
    return res;
}

/**
 * spawn:
 *
 * Runs `argv` in the background, handling any errors that occur
 * when trying to start the program.
 *
 * @param {readonly string[]} argv an argv array
 */
export function spawn(argv) {
    try {
        trySpawn(argv);
    } catch (err) {
        _handleSpawnError(argv[0], err);
    }
}

/**
 * spawnCommandLine:
 *
 * @param {readonly string[]} commandLine a command line
 *
 * Runs commandLine in the background, handling any errors that
 * occur when trying to parse or start the program.
 */
export function spawnCommandLine(commandLine) {
    try {
        let [success_, argv] = GLib.shell_parse_argv(commandLine);
        trySpawn(argv);
    } catch (err) {
        _handleSpawnError(commandLine, err);
    }
}

/**
 * spawnApp:
 *
 * @param {readonly string[]} argv an argv array
 *
 * Runs argv as if it was an application, handling startup notification
 */
export function spawnApp(argv) {
    try {
        const app = Gio.AppInfo.create_from_commandline(argv.join(' '),
            null,
            Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION);

        let context = global.create_app_launch_context(0, -1);
        app.launch([], context);
    } catch (err) {
        _handleSpawnError(argv[0], err);
    }
}

/**
 * trySpawn:
 *
 * @param {readonly string[]} argv an argv array
 *
 * Runs argv in the background. If launching argv fails,
 * this will throw an error.
 */
export function trySpawn(argv) {
    let success_, pid;
    let path = argv[0];

    if (!path?.includes('/')) {
        path = GLib.find_program_in_path(argv[0]);
        if (!path) {
            throw new GLib.SpawnError({
                code: GLib.SpawnError.NOENT,
                message: _('Command not found'),
            });
        }

        argv = [path, ...argv.slice(1)];
    }

    try {
        const launchContext = global.create_app_launch_context(0, -1);
        pid = Shell.util_spawn_async(
            null, argv, launchContext.get_environment(),
            GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD);
    } catch (err) {
        /* Rewrite the error in case of ENOENT */
        if (err.matches(GLib.SpawnError, GLib.SpawnError.NOENT)) {
            throw new GLib.SpawnError({
                code: GLib.SpawnError.NOENT,
                message: _('Command not found'),
            });
        } else if (err instanceof GLib.Error) {
            // The exception from gjs contains an error string like:
            //   Error invoking GLib.spawn_command_line_async: Failed to
            //   execute child process "foo" (No such file or directory)
            // We are only interested in the part in the parentheses. (And
            // we can't pattern match the text, since it gets localized.)
            let message = err.message.replace(/.*\((.+)\)/, '$1');
            throw new err.constructor({code: err.code, message});
        } else {
            throw err;
        }
    }

    if (!path?.includes('/snap/bin') && !argv?.join(' ').includes('snap run')) {
        // Async call, we don't need the reply though
        GnomeDesktop.start_systemd_scope(argv[0], pid, null, null, null, () => {});
    }

    // Dummy child watch; we don't want to double-fork internally
    // because then we lose the parent-child relationship, which
    // can break polkit.  See https://bugzilla.redhat.com//show_bug.cgi?id=819275
    GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, () => {});
}

/**
 * trySpawnCommandLine:
 *
 * @param {readonly string[]} commandLine a command line
 *
 * Runs commandLine in the background. If launching commandLine
 * fails, this will throw an error.
 */
export function trySpawnCommandLine(commandLine) {
    let success_, argv;

    try {
        [success_, argv] = GLib.shell_parse_argv(commandLine);
    } catch (err) {
        // Replace "Error invoking GLib.shell_parse_argv: " with
        // something nicer
        err.message = err.message.replace(/[^:]*: /, `${_('Could not parse command:')}\n`);
        throw err;
    }

    trySpawn(argv);
}

function _handleSpawnError(command, err) {
    let title = _('Execution of “%s” failed:').format(command);
    Main.notifyError(title, err.message);
}

/**
 * Returns an {@link St.Label} with the date passed formatted
 * using {@link formatTime}
 *
 * @param {Date} date the date to format for the label
 * @param {object} params params for {@link formatTime}
 * @returns {St.Label}
 */
export function createTimeLabel(date, params) {
    if (_desktopSettings == null)
        _desktopSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.interface'});

    let label = new St.Label({text: formatTime(date, params)});
    _desktopSettings.connectObject(
        'changed::clock-format', () => (label.text = formatTime(date, params)),
        label);
    return label;
}


/**
 * lowerBound:
 *
 * @template T, [K=T]
 * @param {readonly T[]} array an array or array-like object, already sorted
 *         according to `cmp`
 * @param {K} val the value to add
 * @param {(a: T, val: K) => number} cmp a comparator (or undefined to compare as numbers)
 * @returns {number}
 *
 * Returns the position of the first element that is not
 * lower than `val`, according to `cmp`.
 * That is, returns the first position at which it
 * is possible to insert val without violating the
 * order.
 *
 * This is quite like an ordinary binary search, except
 * that it doesn't stop at first element comparing equal.
 */
function lowerBound(array, val, cmp) {
    let min, max, mid, v;
    cmp ||= (a, b) => a - b;

    if (array.length === 0)
        return 0;

    min = 0;
    max = array.length;
    while (min < (max - 1)) {
        mid = Math.floor((min + max) / 2);
        v = cmp(array[mid], val);

        if (v < 0)
            min = mid + 1;
        else
            max = mid;
    }

    return min === max || cmp(array[min], val) < 0 ? max : min;
}

/**
 * insertSorted:
 *
 * @template T, [K=T]
 * @param {T[]} array an array sorted according to `cmp`
 * @param {K} val a value to insert
 * @param {(a: T, val: K) => number} cmp the sorting function
 * @returns {number}
 *
 * Inserts `val` into `array`, preserving the
 * sorting invariants.
 *
 * Returns the position at which it was inserted
 */
export function insertSorted(array, val, cmp) {
    let pos = lowerBound(array, val, cmp);
    array.splice(pos, 0, val);

    return pos;
}

/**
 * @param {number} start
 * @param {number} end
 * @param {number} progress
 * @returns {number}
 */
export function lerp(start, end, progress) {
    return start + progress * (end - start);
}

/**
 * _GNOMEversionToNumber:
 *
 * @param {string} version a GNOME version element
 * @returns {number}
 *
 * Like Number() but returns sortable values for special-cases
 * 'alpha' and 'beta'. Returns NaN for unhandled 'versions'.
 */
function _GNOMEversionToNumber(version) {
    let ret = Number(version);
    if (!isNaN(ret))
        return ret;
    if (version === 'alpha')
        return -3;
    if (version === 'beta')
        return -2;
    if (version === 'rc')
        return -1;
    return ret;
}

/**
 * GNOMEversionCompare:
 *
 * @param {string} version1 a string containing a GNOME version
 * @param {string} version2 a string containing another GNOME version
 * @returns {number}
 *
 * Returns an integer less than, equal to, or greater than
 * zero, if `version1` is older, equal or newer than `version2`
 */
export function GNOMEversionCompare(version1, version2) {
    const v1Array = version1.split('.');
    const v2Array = version2.split('.');

    for (let i = 0; i < Math.max(v1Array.length, v2Array.length); i++) {
        let elemV1 = _GNOMEversionToNumber(v1Array[i] || '0');
        let elemV2 = _GNOMEversionToNumber(v2Array[i] || '0');
        if (elemV1 < elemV2)
            return -1;
        if (elemV1 > elemV2)
            return 1;
    }

    return 0;
}

export class DBusSenderChecker {
    /**
     * @param {string[]} allowList - list of allowed well-known names
     */
    constructor(allowList) {
        this._allowlistMap = new Map();

        this._uninitializedNames = new Set(allowList);
        this._initializedPromise = new Promise(resolve => {
            this._resolveInitialized = resolve;
        });

        this._watchList = allowList.map(name => {
            return Gio.DBus.watch_name(Gio.BusType.SESSION,
                name,
                Gio.BusNameWatcherFlags.NONE,
                (conn_, name_, owner) => {
                    this._allowlistMap.set(name, owner);
                    this._checkAndResolveInitialized(name);
                },
                () => {
                    this._allowlistMap.delete(name);
                    this._checkAndResolveInitialized(name);
                });
        });
    }

    /**
     * @param {string} name - bus name for which the watcher got initialized
     */
    _checkAndResolveInitialized(name) {
        if (this._uninitializedNames.delete(name) &&
            this._uninitializedNames.size === 0)
            this._resolveInitialized();
    }

    /**
     * @async
     * @param {string} sender - the bus name that invoked the checked method
     * @returns {bool}
     */
    async _isSenderAllowed(sender) {
        await this._initializedPromise;
        return [...this._allowlistMap.values()].includes(sender);
    }

    /**
     * Check whether the bus name that invoked @invocation maps
     * to an entry in the allow list.
     *
     * @async
     * @throws
     * @param {Gio.DBusMethodInvocation} invocation - the invocation
     * @returns {void}
     */
    async checkInvocation(invocation) {
        if (global.context.unsafe_mode)
            return;

        if (await this._isSenderAllowed(invocation.get_sender()))
            return;

        throw new GLib.Error(Gio.DBusError,
            Gio.DBusError.ACCESS_DENIED,
            `${invocation.get_method_name()} is not allowed`);
    }

    /**
     * @returns {void}
     */
    destroy() {
        for (const id in this._watchList)
            Gio.DBus.unwatch_name(id);
        this._watchList = [];
    }
}

/* @class Highlighter Highlight given terms in text using markup. */
export class Highlighter {
    /**
     * @param {?string[]} terms - list of terms to highlight
     */
    constructor(terms) {
        if (!terms)
            return;

        const escapedTerms = terms
            .map(term => Shell.util_regex_escape(term))
            .filter(term => term.length > 0);

        if (escapedTerms.length === 0)
            return;

        this._highlightRegex = new RegExp(
            `(${escapedTerms.join('|')})`, 'gi');
    }

    /**
     * Highlight all occurences of the terms defined for this
     * highlighter in the provided text using markup.
     *
     * @param {string} text - text to highlight the defined terms in
     * @returns {string}
     */
    highlight(text) {
        if (!this._highlightRegex)
            return GLib.markup_escape_text(text, -1);

        let escaped = [];
        let lastMatchEnd = 0;
        let match;
        while ((match = this._highlightRegex.exec(text))) {
            if (match.index > lastMatchEnd) {
                let unmatched = GLib.markup_escape_text(
                    text.slice(lastMatchEnd, match.index), -1);
                escaped.push(unmatched);
            }
            let matched = GLib.markup_escape_text(match[0], -1);
            escaped.push(`<b>${matched}</b>`);
            lastMatchEnd = match.index + match[0].length;
        }
        let unmatched = GLib.markup_escape_text(
            text.slice(lastMatchEnd), -1);
        escaped.push(unmatched);

        return escaped.join('');
    }
}
(uuay)osdWindow.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import St from 'gi://St';

import * as BarLevel from './barLevel.js';
import * as Layout from './layout.js';
import * as Main from './main.js';

const HIDE_TIMEOUT = 1500;
const FADE_TIME = 100;
const LEVEL_ANIMATION_TIME = 100;

export const OsdWindow = GObject.registerClass(
class OsdWindow extends Clutter.Actor {
    _init(monitorIndex) {
        super._init({
            x_expand: true,
            y_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.END,
        });

        this._monitorIndex = monitorIndex;
        let constraint = new Layout.MonitorConstraint({index: monitorIndex});
        this.add_constraint(constraint);

        this._hbox = new St.BoxLayout({
            style_class: 'osd-window',
        });
        this.add_child(this._hbox);

        this._icon = new St.Icon({y_expand: true});
        this._hbox.add_child(this._icon);

        this._vbox = new St.BoxLayout({
            vertical: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._hbox.add_child(this._vbox);

        this._label = new St.Label();
        this._vbox.add_child(this._label);

        this._level = new BarLevel.BarLevel({
            style_class: 'level',
            value: 0,
        });
        this._vbox.add_child(this._level);

        this._hideTimeoutId = 0;
        this._reset();
        Main.uiGroup.add_child(this);
    }

    _updateBoxVisibility() {
        this._vbox.visible = [...this._vbox].some(c => c.visible);
    }

    setIcon(icon) {
        this._icon.gicon = icon;
    }

    setLabel(label) {
        this._label.visible = label != null;
        if (this._label.visible)
            this._label.text = label;
        this._updateBoxVisibility();
    }

    setLevel(value) {
        this._level.visible = value != null;
        if (this._level.visible) {
            if (this.visible) {
                this._level.ease_property('value', value, {
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                    duration: LEVEL_ANIMATION_TIME,
                });
            } else {
                this._level.value = value;
            }
        }
        this._updateBoxVisibility();
    }

    setMaxLevel(maxLevel = 1) {
        this._level.maximum_value = maxLevel;
    }

    show() {
        if (!this._icon.gicon)
            return;

        if (!this.visible) {
            Meta.disable_unredirect_for_display(global.display);
            super.show();
            this.opacity = 0;
            this.get_parent().set_child_above_sibling(this, null);

            this.ease({
                opacity: 255,
                duration: FADE_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            });
        }

        if (this._hideTimeoutId)
            GLib.source_remove(this._hideTimeoutId);
        this._hideTimeoutId = GLib.timeout_add(
            GLib.PRIORITY_DEFAULT, HIDE_TIMEOUT, this._hide.bind(this));
        GLib.Source.set_name_by_id(this._hideTimeoutId, '[gnome-shell] this._hide');
    }

    cancel() {
        if (!this._hideTimeoutId)
            return;

        GLib.source_remove(this._hideTimeoutId);
        this._hide();
    }

    _hide() {
        this._hideTimeoutId = 0;
        this.ease({
            opacity: 0,
            duration: FADE_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this._reset();
                Meta.enable_unredirect_for_display(global.display);
            },
        });
        return GLib.SOURCE_REMOVE;
    }

    _reset() {
        super.hide();
        this.setLabel(null);
        this.setMaxLevel(null);
        this.setLevel(null);
    }
});

export class OsdWindowManager {
    constructor() {
        this._osdWindows = [];
        Main.layoutManager.connect('monitors-changed',
            this._monitorsChanged.bind(this));
        this._monitorsChanged();
    }

    _monitorsChanged() {
        for (let i = 0; i < Main.layoutManager.monitors.length; i++) {
            if (this._osdWindows[i] === undefined)
                this._osdWindows[i] = new OsdWindow(i);
        }

        for (let i = Main.layoutManager.monitors.length; i < this._osdWindows.length; i++) {
            this._osdWindows[i].destroy();
            this._osdWindows[i] = null;
        }

        this._osdWindows.length = Main.layoutManager.monitors.length;
    }

    _showOsdWindow(monitorIndex, icon, label, level, maxLevel) {
        this._osdWindows[monitorIndex].setIcon(icon);
        this._osdWindows[monitorIndex].setLabel(label);
        this._osdWindows[monitorIndex].setMaxLevel(maxLevel);
        this._osdWindows[monitorIndex].setLevel(level);
        this._osdWindows[monitorIndex].show();
    }

    show(monitorIndex, icon, label, level, maxLevel) {
        if (monitorIndex !== -1) {
            for (let i = 0; i < this._osdWindows.length; i++) {
                if (i === monitorIndex)
                    this._showOsdWindow(i, icon, label, level, maxLevel);
                else
                    this._osdWindows[i].cancel();
            }
        } else {
            for (let i = 0; i < this._osdWindows.length; i++)
                this._showOsdWindow(i, icon, label, level, maxLevel);
        }
    }

    hideAll() {
        for (let i = 0; i < this._osdWindows.length; i++)
            this._osdWindows[i].cancel();
    }
}
(uuay)location.js.// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Dialog from '../dialog.js';
import * as ModalDialog from '../modalDialog.js';
import * as PermissionStore from '../../misc/permissionStore.js';
import {SystemIndicator} from '../quickSettings.js';

import {loadInterfaceXML} from '../../misc/fileUtils.js';

const LOCATION_SCHEMA = 'org.gnome.system.location';
const MAX_ACCURACY_LEVEL = 'max-accuracy-level';
const ENABLED = 'enabled';

const APP_PERMISSIONS_TABLE = 'location';
const APP_PERMISSIONS_ID = 'location';

/** @enum {number} */
const GeoclueAccuracyLevel = {
    NONE: 0,
    COUNTRY: 1,
    CITY: 4,
    NEIGHBORHOOD: 5,
    STREET: 6,
    EXACT: 8,
};

function accuracyLevelToString(accuracyLevel) {
    for (let key in GeoclueAccuracyLevel) {
        if (GeoclueAccuracyLevel[key] === accuracyLevel)
            return key;
    }

    return 'NONE';
}

const GeoclueIface = loadInterfaceXML('org.freedesktop.GeoClue2.Manager');
const GeoclueManager = Gio.DBusProxy.makeProxyWrapper(GeoclueIface);

const AgentIface = loadInterfaceXML('org.freedesktop.GeoClue2.Agent');

let _geoclueAgent = null;

/**
 * @returns {GeoclueAgent} - the GeoclueAgent singleton
 */
export function getGeoclueAgent() {
    if (_geoclueAgent === null)
        _geoclueAgent = new GeoclueAgent();
    return _geoclueAgent;
}

const GeoclueAgent = GObject.registerClass({
    Properties: {
        'enabled': GObject.ParamSpec.boolean(
            'enabled', 'Enabled', 'Enabled',
            GObject.ParamFlags.READWRITE,
            false),
        'in-use': GObject.ParamSpec.boolean(
            'in-use', 'In use', 'In use',
            GObject.ParamFlags.READABLE,
            false),
        'max-accuracy-level': GObject.ParamSpec.int(
            'max-accuracy-level', 'Max accuracy level', 'Max accuracy level',
            GObject.ParamFlags.READABLE,
            0, 8, 0),
    },
}, class GeoclueAgent extends GObject.Object {
    _init() {
        super._init();

        this._settings = new Gio.Settings({schema_id: LOCATION_SCHEMA});
        this._settings.connectObject(
            `changed::${ENABLED}`, () => this.notify('enabled'),
            `changed::${MAX_ACCURACY_LEVEL}`, () => this._onMaxAccuracyLevelChanged(),
            this);

        this._agent = Gio.DBusExportedObject.wrapJSObject(AgentIface, this);
        this._agent.export(Gio.DBus.system, '/org/freedesktop/GeoClue2/Agent');

        this.connect('notify::enabled', this._onMaxAccuracyLevelChanged.bind(this));

        this._watchId = Gio.bus_watch_name(Gio.BusType.SYSTEM,
            'org.freedesktop.GeoClue2',
            0,
            this._connectToGeoclue.bind(this),
            this._onGeoclueVanished.bind(this));
        this._onMaxAccuracyLevelChanged();
        this._connectToGeoclue();
        this._connectToPermissionStore();
    }

    get enabled() {
        return this._settings.get_boolean(ENABLED);
    }

    set enabled(value) {
        this._settings.set_boolean(ENABLED, value);
    }

    get inUse() {
        return this._managerProxy?.InUse ?? false;
    }

    get maxAccuracyLevel() {
        if (this.enabled) {
            let level = this._settings.get_string(MAX_ACCURACY_LEVEL);

            return GeoclueAccuracyLevel[level.toUpperCase()] ||
                   GeoclueAccuracyLevel.NONE;
        } else {
            return GeoclueAccuracyLevel.NONE;
        }
    }

    async AuthorizeAppAsync(params, invocation) {
        let [desktopId, reqAccuracyLevel] = params;

        let authorizer = new AppAuthorizer(desktopId,
            reqAccuracyLevel, this._permStoreProxy, this.maxAccuracyLevel);

        const accuracyLevel = await authorizer.authorize();
        const ret = accuracyLevel !== GeoclueAccuracyLevel.NONE;
        invocation.return_value(GLib.Variant.new('(bu)', [ret, accuracyLevel]));
    }

    get MaxAccuracyLevel() {
        return this.maxAccuracyLevel;
    }

    _connectToGeoclue() {
        if (this._managerProxy != null || this._connecting)
            return false;

        this._connecting = true;
        new GeoclueManager(Gio.DBus.system,
            'org.freedesktop.GeoClue2',
            '/org/freedesktop/GeoClue2/Manager',
            this._onManagerProxyReady.bind(this));
        return true;
    }

    async _onManagerProxyReady(proxy, error) {
        if (error != null) {
            log(error.message);
            this._connecting = false;
            return;
        }

        this._managerProxy = proxy;
        this._managerProxy.connectObject('g-properties-changed',
            this._onGeocluePropsChanged.bind(this), this);

        this.notify('in-use');

        try {
            await this._managerProxy.AddAgentAsync('gnome-shell');
            this._connecting = false;
            this._notifyMaxAccuracyLevel();
        } catch (e) {
            log(e.message);
        }
    }

    _onGeoclueVanished() {
        this._managerProxy?.disconnectObject(this);
        this._managerProxy = null;

        this.notify('in-use');
    }

    _onMaxAccuracyLevelChanged() {
        this.notify('max-accuracy-level');

        // Gotta ensure geoclue is up and we are registered as agent to it
        // before we emit the notify for this property change.
        if (!this._connectToGeoclue())
            this._notifyMaxAccuracyLevel();
    }

    _notifyMaxAccuracyLevel() {
        let variant = new GLib.Variant('u', this.maxAccuracyLevel);
        this._agent.emit_property_changed('MaxAccuracyLevel', variant);
    }

    _onGeocluePropsChanged(proxy, properties) {
        const inUseChanged = !!properties.lookup_value('InUse', null);
        if (inUseChanged)
            this.notify('in-use');
    }

    _connectToPermissionStore() {
        this._permStoreProxy = null;
        new PermissionStore.PermissionStore(this._onPermStoreProxyReady.bind(this));
    }

    _onPermStoreProxyReady(proxy, error) {
        if (error != null) {
            log(error.message);
            return;
        }

        this._permStoreProxy = proxy;
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this._agent = getGeoclueAgent();

        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'location-services-active-symbolic';
        this._agent.bind_property('in-use',
            this._indicator,
            'visible',
            GObject.BindingFlags.SYNC_CREATE);
    }
});

class AppAuthorizer {
    constructor(desktopId, reqAccuracyLevel, permStoreProxy, maxAccuracyLevel) {
        this.desktopId = desktopId;
        this.reqAccuracyLevel = reqAccuracyLevel;
        this._permStoreProxy = permStoreProxy;
        this._maxAccuracyLevel = maxAccuracyLevel;
        this._permissions = {};

        this._accuracyLevel = GeoclueAccuracyLevel.NONE;
    }

    async authorize() {
        let appSystem = Shell.AppSystem.get_default();
        this._app = appSystem.lookup_app(`${this.desktopId}.desktop`);
        if (this._app == null || this._permStoreProxy == null)
            return this._completeAuth();

        try {
            [this._permissions] = await this._permStoreProxy.LookupAsync(
                APP_PERMISSIONS_TABLE,
                APP_PERMISSIONS_ID);
        } catch (error) {
            if (error.domain === Gio.DBusError) {
                // Likely no xdg-app installed, just authorize the app
                this._accuracyLevel = this.reqAccuracyLevel;
                this._permStoreProxy = null;
                return this._completeAuth();
            } else {
                // Currently xdg-app throws an error if we lookup for
                // unknown ID (which would be the case first time this code
                // runs) so we continue with user authorization as normal
                // and ID is added to the store if user says "yes".
                log(error.message);
                this._permissions = {};
            }
        }

        let permission = this._permissions[this.desktopId];

        if (permission == null) {
            await this._userAuthorizeApp();
        } else {
            let [levelStr] = permission || ['NONE'];
            this._accuracyLevel = GeoclueAccuracyLevel[levelStr] ||
                                  GeoclueAccuracyLevel.NONE;
        }

        return this._completeAuth();
    }

    _userAuthorizeApp() {
        let name = this._app.get_name();
        let appInfo = this._app.get_app_info();
        let reason = appInfo.get_locale_string('X-Geoclue-Reason');

        this._dialog =
            new GeolocationDialog(name, reason, this.reqAccuracyLevel);

        return new Promise(resolve => {
            const responseId = this._dialog.connect('response',
                (dialog, level) => {
                    this._dialog.disconnect(responseId);
                    this._accuracyLevel = level;
                    resolve();
                });
            this._dialog.open();
        });
    }

    _completeAuth() {
        if (this._accuracyLevel !== GeoclueAccuracyLevel.NONE) {
            this._accuracyLevel = Math.clamp(this._accuracyLevel,
                0, this._maxAccuracyLevel);
        }
        this._saveToPermissionStore();

        return this._accuracyLevel;
    }

    async _saveToPermissionStore() {
        if (this._permStoreProxy == null)
            return;

        let levelStr = accuracyLevelToString(this._accuracyLevel);
        let dateStr = Math.round(Date.now() / 1000).toString();
        this._permissions[this.desktopId] = [levelStr, dateStr];

        let data = GLib.Variant.new('av', {});

        try {
            await this._permStoreProxy.SetAsync(
                APP_PERMISSIONS_TABLE,
                true,
                APP_PERMISSIONS_ID,
                this._permissions,
                data);
        } catch (error) {
            log(error.message);
        }
    }
}

export const GeolocationDialog = GObject.registerClass({
    Signals: {'response': {param_types: [GObject.TYPE_UINT]}},
}, class GeolocationDialog extends ModalDialog.ModalDialog {
    _init(name, reason, reqAccuracyLevel) {
        super._init({styleClass: 'geolocation-dialog'});
        this.reqAccuracyLevel = reqAccuracyLevel;

        let content = new Dialog.MessageDialogContent({
            title: _('Allow location access'),
            /* Translators: %s is an application name */
            description: _('The app %s wants to access your location').format(name),
        });

        let reasonLabel = new St.Label({
            text: reason,
            style_class: 'message-dialog-description',
        });
        content.add_child(reasonLabel);

        let infoLabel = new St.Label({
            text: _('Location access can be changed at any time from the privacy settings.'),
            style_class: 'message-dialog-description',
        });
        content.add_child(infoLabel);

        this.contentLayout.add_child(content);

        const button = this.addButton({
            label: _('Deny Access'),
            action: this._onDenyClicked.bind(this),
            key: Clutter.KEY_Escape,
        });
        this.addButton({
            label: _('Grant Access'),
            action: this._onGrantClicked.bind(this),
        });

        this.setInitialKeyFocus(button);
    }

    _onGrantClicked() {
        this.emit('response', this.reqAccuracyLevel);
        this.close();
    }

    _onDenyClicked() {
        this.emit('response', GeoclueAccuracyLevel.NONE);
        this.close();
    }
});
(uuay)workspace.js��// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Background from './background.js';
import * as DND from './dnd.js';
import * as Main from './main.js';
import * as OverviewControls from './overviewControls.js';
import * as Params from '../misc/params.js';

import * as Util from '../misc/util.js';
import {WindowPreview} from './windowPreview.js';

const WINDOW_PREVIEW_MAXIMUM_SCALE = 0.95;

const WINDOW_REPOSITIONING_DELAY = 750;

// When calculating a layout, we calculate the scale of windows and the percent
// of the available area the new layout uses. If the values for the new layout,
// when weighted with the values as below, are worse than the previous layout's,
// we stop looking for a new layout and use the previous layout.
// Otherwise, we keep looking for a new layout.
const LAYOUT_SCALE_WEIGHT = 1;
const LAYOUT_SPACE_WEIGHT = 0.1;

const BACKGROUND_CORNER_RADIUS_PIXELS = 30;

// Window Thumbnail Layout Algorithm
// =================================
//
// General overview
// ----------------
//
// The window thumbnail layout algorithm calculates some optimal layout
// by computing layouts with some number of rows, calculating how good
// each layout is, and stopping iterating when it finds one that is worse
// than the previous layout. A layout consists of which windows are in
// which rows, row sizes and other general state tracking that would make
// calculating window positions from this information fairly easy.
//
// After a layout is computed that's considered the best layout, we
// compute the layout scale to fit it in the area, and then compute
// slots (sizes and positions) for each thumbnail.
//
// Layout generation
// -----------------
//
// Layout generation is naive and simple: we simply add windows to a row
// until we've added too many windows to a row, and then make a new row,
// until we have our required N rows. The potential issue with this strategy
// is that we may have too many windows at the bottom in some pathological
// cases, which tends to make the thumbnails have the shape of a pile of
// sand with a peak, with one window at the top.
//
// Scaling factors
// ---------------
//
// Thumbnail position is mostly straightforward -- the main issue is
// computing an optimal scale for each window that fits the constraints,
// and doesn't make the thumbnail too small to see. There are two factors
// involved in thumbnail scale to make sure that these two goals are met:
// the window scale (calculated by _computeWindowScale) and the layout
// scale (calculated by computeSizeAndScale).
//
// The calculation logic becomes slightly more complicated because row
// and column spacing are not scaled, they're constant, so we can't
// simply generate a bunch of window positions and then scale it. In
// practice, it's not too bad -- we can simply try to fit the layout
// in the input area minus whatever spacing we have, and then add
// it back afterwards.
//
// The window scale is constant for the window's size regardless of the
// input area or the layout scale or rows or anything else, and right
// now just enlarges the window if it's too small. The fact that this
// factor is stable makes it easy to calculate, so there's no sense
// in not applying it in most calculations.
//
// The layout scale depends on the input area, the rows, etc, but is the
// same for the entire layout, rather than being per-window. After
// generating the rows of windows, we basically do some basic math to
// fit the full, unscaled layout to the input area, as described above.
//
// With these two factors combined, the final scale of each thumbnail is
// simply windowScale * layoutScale... almost.
//
// There's one additional constraint: the thumbnail scale must never be
// larger than WINDOW_PREVIEW_MAXIMUM_SCALE, which means that the inequality:
//
//   windowScale * layoutScale <= WINDOW_PREVIEW_MAXIMUM_SCALE
//
// must always be true. This is for each individual window -- while we
// could adjust layoutScale to make the largest thumbnail smaller than
// WINDOW_PREVIEW_MAXIMUM_SCALE, it would shrink windows which are already
// under the inequality. To solve this, we simply cheat: we simply keep
// each window's "cell" area to be the same, but we shrink the thumbnail
// and center it horizontally, and align it to the bottom vertically.

export class LayoutStrategy {
    constructor(params) {
        params = Params.parse(params, {
            monitor: null,
            rowSpacing: 0,
            columnSpacing: 0,
        });

        if (!params.monitor)
            throw new Error(`No monitor param passed to ${this.constructor.name}`);

        this._monitor = params.monitor;
        this._rowSpacing = params.rowSpacing;
        this._columnSpacing = params.columnSpacing;
    }

    // Compute a strategy-specific overall layout given a list of WindowPreviews
    // @windows and the strategy-specific @layoutParams.
    //
    // Returns a strategy-specific layout object that is opaque to the user.
    computeLayout(_windows, _layoutParams) {
        throw new GObject.NotImplementedError(`computeLayout in ${this.constructor.name}`);
    }

    // Given @layout and @area, compute the overall scale of the layout and
    // space occupied by the layout.
    //
    // This method returns an array where the first element is the scale and
    // the second element is the space.
    //
    // This method must be called before calling computeWindowSlots(), as it
    // sets the fixed overall scale of the layout.
    computeScaleAndSpace(_layout, _area) {
        throw new GObject.NotImplementedError(`computeScaleAndSpace in ${this.constructor.name}`);
    }

    // Returns an array with final position and size information for each
    // window of the layout, given a bounding area that it will be inside of.
    computeWindowSlots(_layout, _area) {
        throw new GObject.NotImplementedError(`computeWindowSlots in ${this.constructor.name}`);
    }
}

class UnalignedLayoutStrategy extends LayoutStrategy {
    _newRow() {
        // Row properties:
        //
        // * x, y are the position of row, relative to area
        //
        // * width, height are the scaled versions of fullWidth, fullHeight
        //
        // * width also has the spacing in between windows. It's not in
        //   fullWidth, as the spacing is constant, whereas fullWidth is
        //   meant to be scaled
        //
        // * neither height/fullHeight have any sort of spacing or padding
        return {
            x: 0, y: 0,
            width: 0, height: 0,
            fullWidth: 0, fullHeight: 0,
            windows: [],
        };
    }

    // Computes and returns an individual scaling factor for @window,
    // to be applied in addition to the overall layout scale.
    _computeWindowScale(window) {
        // Since we align windows next to each other, the height of the
        // thumbnails is much more important to preserve than the width of
        // them, so two windows with equal height, but maybe differering
        // widths line up.
        let ratio = window.boundingBox.height / this._monitor.height;

        // The purpose of this manipulation here is to prevent windows
        // from getting too small. For something like a calculator window,
        // we need to bump up the size just a bit to make sure it looks
        // good. We'll use a multiplier of 1.5 for this.

        // Map from [0, 1] to [1.5, 1]
        return Util.lerp(1.5, 1, ratio);
    }

    _computeRowSizes(layout) {
        let {rows, scale} = layout;
        for (let i = 0; i < rows.length; i++) {
            let row = rows[i];
            row.width = row.fullWidth * scale + (row.windows.length - 1) * this._columnSpacing;
            row.height = row.fullHeight * scale;
        }
    }

    _keepSameRow(row, window, width, idealRowWidth) {
        if (row.fullWidth + width <= idealRowWidth)
            return true;

        let oldRatio = row.fullWidth / idealRowWidth;
        let newRatio = (row.fullWidth + width) / idealRowWidth;

        if (Math.abs(1 - newRatio) < Math.abs(1 - oldRatio))
            return true;

        return false;
    }

    _sortRow(row) {
        // Sort windows horizontally to minimize travel distance.
        // This affects in what order the windows end up in a row.
        row.windows.sort((a, b) => a.windowCenter.x - b.windowCenter.x);
    }

    computeLayout(windows, layoutParams) {
        layoutParams = Params.parse(layoutParams, {
            numRows: 0,
        });

        if (layoutParams.numRows === 0)
            throw new Error(`${this.constructor.name}: No numRows given in layout params`);

        const numRows = layoutParams.numRows;

        let rows = [];
        let totalWidth = 0;
        for (let i = 0; i < windows.length; i++) {
            let window = windows[i];
            let s = this._computeWindowScale(window);
            totalWidth += window.boundingBox.width * s;
        }

        let idealRowWidth = totalWidth / numRows;

        // Sort windows vertically to minimize travel distance.
        // This affects what rows the windows get placed in.
        let sortedWindows = windows.slice();
        sortedWindows.sort((a, b) => a.windowCenter.y - b.windowCenter.y);

        let windowIdx = 0;
        for (let i = 0; i < numRows; i++) {
            let row = this._newRow();
            rows.push(row);

            for (; windowIdx < sortedWindows.length; windowIdx++) {
                let window = sortedWindows[windowIdx];
                let s = this._computeWindowScale(window);
                let width = window.boundingBox.width * s;
                let height = window.boundingBox.height * s;
                row.fullHeight = Math.max(row.fullHeight, height);

                // either new width is < idealWidth or new width is nearer from idealWidth then oldWidth
                if (this._keepSameRow(row, window, width, idealRowWidth) || (i === numRows - 1)) {
                    row.windows.push(window);
                    row.fullWidth += width;
                } else {
                    break;
                }
            }
        }

        let gridHeight = 0;
        let maxRow;
        for (let i = 0; i < numRows; i++) {
            let row = rows[i];
            this._sortRow(row);

            if (!maxRow || row.fullWidth > maxRow.fullWidth)
                maxRow = row;
            gridHeight += row.fullHeight;
        }

        return {
            numRows,
            rows,
            maxColumns: maxRow.windows.length,
            gridWidth: maxRow.fullWidth,
            gridHeight,
        };
    }

    computeScaleAndSpace(layout, area) {
        let hspacing = (layout.maxColumns - 1) * this._columnSpacing;
        let vspacing = (layout.numRows - 1) * this._rowSpacing;

        let spacedWidth = area.width - hspacing;
        let spacedHeight = area.height - vspacing;

        let horizontalScale = spacedWidth / layout.gridWidth;
        let verticalScale = spacedHeight / layout.gridHeight;

        // Thumbnails should be less than 70% of the original size
        let scale = Math.min(
            horizontalScale, verticalScale, WINDOW_PREVIEW_MAXIMUM_SCALE);

        let scaledLayoutWidth = layout.gridWidth * scale + hspacing;
        let scaledLayoutHeight = layout.gridHeight * scale + vspacing;
        let space = (scaledLayoutWidth * scaledLayoutHeight) / (area.width * area.height);

        layout.scale = scale;

        return [scale, space];
    }

    computeWindowSlots(layout, area) {
        this._computeRowSizes(layout);

        let {rows, scale} = layout;

        let slots = [];

        // Do this in three parts.
        let heightWithoutSpacing = 0;
        for (let i = 0; i < rows.length; i++) {
            let row = rows[i];
            heightWithoutSpacing += row.height;
        }

        let verticalSpacing = (rows.length - 1) * this._rowSpacing;
        let additionalVerticalScale = Math.min(1, (area.height - verticalSpacing) / heightWithoutSpacing);

        // keep track how much smaller the grid becomes due to scaling
        // so it can be centered again
        let compensation = 0;
        let y = 0;

        for (let i = 0; i < rows.length; i++) {
            let row = rows[i];

            // If this window layout row doesn't fit in the actual
            // geometry, then apply an additional scale to it.
            let horizontalSpacing = (row.windows.length - 1) * this._columnSpacing;
            let widthWithoutSpacing = row.width - horizontalSpacing;
            let additionalHorizontalScale = Math.min(1, (area.width - horizontalSpacing) / widthWithoutSpacing);

            if (additionalHorizontalScale < additionalVerticalScale) {
                row.additionalScale = additionalHorizontalScale;
                // Only consider the scaling in addition to the vertical scaling for centering.
                compensation += (additionalVerticalScale - additionalHorizontalScale) * row.height;
            } else {
                row.additionalScale = additionalVerticalScale;
                // No compensation when scaling vertically since centering based on a too large
                // height would undo what vertical scaling is trying to achieve.
            }

            row.x = area.x + (Math.max(area.width - (widthWithoutSpacing * row.additionalScale + horizontalSpacing), 0) / 2);
            row.y = area.y + (Math.max(area.height - (heightWithoutSpacing + verticalSpacing), 0) / 2) + y;
            y += row.height * row.additionalScale + this._rowSpacing;
        }

        compensation /= 2;

        for (let i = 0; i < rows.length; i++) {
            const row = rows[i];
            const rowY = row.y + compensation;
            const rowHeight = row.height * row.additionalScale;

            let x = row.x;
            for (let j = 0; j < row.windows.length; j++) {
                let window = row.windows[j];

                let s = scale * this._computeWindowScale(window) * row.additionalScale;
                let cellWidth = window.boundingBox.width * s;
                let cellHeight = window.boundingBox.height * s;

                s = Math.min(s, WINDOW_PREVIEW_MAXIMUM_SCALE);
                let cloneWidth = window.boundingBox.width * s;
                const cloneHeight = window.boundingBox.height * s;

                let cloneX = x + (cellWidth - cloneWidth) / 2;
                let cloneY;

                // If there's only one row, align windows vertically centered inside the row
                if (rows.length === 1)
                    cloneY = rowY + (rowHeight - cloneHeight) / 2;
                // If there are multiple rows, align windows to the bottom edge of the row
                else
                    cloneY = rowY + rowHeight - cellHeight;

                // Align with the pixel grid to prevent blurry windows at scale = 1
                cloneX = Math.floor(cloneX);
                cloneY = Math.floor(cloneY);

                slots.push([cloneX, cloneY, cloneWidth, cloneHeight, window]);
                x += cellWidth + this._columnSpacing;
            }
        }
        return slots;
    }
}

function animateAllocation(actor, box) {
    actor.save_easing_state();
    actor.set_easing_mode(Clutter.AnimationMode.EASE_OUT_QUAD);
    actor.set_easing_duration(200);

    actor.allocate(box);

    actor.restore_easing_state();

    return actor.get_transition('allocation');
}

export const WorkspaceLayout = GObject.registerClass({
    Properties: {
        'spacing': GObject.ParamSpec.double(
            'spacing', 'Spacing', 'Spacing',
            GObject.ParamFlags.READWRITE,
            0, Infinity, 20),
        'layout-frozen': GObject.ParamSpec.boolean(
            'layout-frozen', 'Layout frozen', 'Layout frozen',
            GObject.ParamFlags.READWRITE,
            false),
    },
}, class WorkspaceLayout extends Clutter.LayoutManager {
    _init(metaWorkspace, monitorIndex, overviewAdjustment) {
        super._init();

        this._spacing = 20;
        this._layoutFrozen = false;

        this._metaWorkspace = metaWorkspace;
        this._monitorIndex = monitorIndex;
        this._overviewAdjustment = overviewAdjustment;

        this._container = null;
        this._windows = new Map();
        this._sortedWindows = [];
        this._lastBox = null;
        this._windowSlots = [];
        this._layout = null;

        this._needsLayout = true;

        this._stateAdjustment = new St.Adjustment({
            value: 0,
            lower: 0,
            upper: 1,
        });

        this._stateAdjustment.connect('notify::value', () => {
            this._syncOpacities();
            this.syncOverlays();
            this.layout_changed();
        });

        this._workarea = null;
        this._workareasChangedId = 0;
    }

    _syncOpacity(actor, metaWindow) {
        if (!metaWindow.showing_on_its_workspace())
            actor.opacity = this._stateAdjustment.value * 255;
    }

    _syncOpacities() {
        this._windows.forEach(({metaWindow}, actor) => {
            this._syncOpacity(actor, metaWindow);
        });
    }

    _isBetterScaleAndSpace(oldScale, oldSpace, scale, space) {
        let spacePower = (space - oldSpace) * LAYOUT_SPACE_WEIGHT;
        let scalePower = (scale - oldScale) * LAYOUT_SCALE_WEIGHT;

        if (scale > oldScale && space > oldSpace) {
            // Win win -- better scale and better space
            return true;
        } else if (scale > oldScale && space <= oldSpace) {
            // Keep new layout only if scale gain outweighs aspect space loss
            return scalePower > spacePower;
        } else if (scale <= oldScale && space > oldSpace) {
            // Keep new layout only if aspect space gain outweighs scale loss
            return spacePower > scalePower;
        } else {
            // Lose -- worse scale and space
            return false;
        }
    }

    _adjustSpacingAndPadding(rowSpacing, colSpacing, containerBox) {
        if (this._sortedWindows.length === 0)
            return [rowSpacing, colSpacing, containerBox];

        // All of the overlays have the same chrome sizes,
        // so just pick the first one.
        const window = this._sortedWindows[0];

        const [topOversize, bottomOversize] = window.chromeHeights();
        const [leftOversize, rightOversize] = window.chromeWidths();

        const oversize =
            Math.max(topOversize, bottomOversize, leftOversize, rightOversize);

        if (rowSpacing !== null)
            rowSpacing += oversize;
        if (colSpacing !== null)
            colSpacing += oversize;

        if (containerBox) {
            const monitor = Main.layoutManager.monitors[this._monitorIndex];

            const bottomPoint = new Graphene.Point3D({y: containerBox.y2});
            const transformedBottomPoint =
                this._container.apply_transform_to_point(bottomPoint);
            const bottomFreeSpace =
                (monitor.y + monitor.height) - transformedBottomPoint.y;

            const [, bottomOverlap] = window.overlapHeights();

            if ((bottomOverlap + oversize) > bottomFreeSpace)
                containerBox.y2 -= (bottomOverlap + oversize) - bottomFreeSpace;
        }

        return [rowSpacing, colSpacing, containerBox];
    }

    _createBestLayout(area) {
        const [rowSpacing, columnSpacing] =
            this._adjustSpacingAndPadding(this._spacing, this._spacing, null);

        // We look for the largest scale that allows us to fit the
        // largest row/tallest column on the workspace.
        this._layoutStrategy = new UnalignedLayoutStrategy({
            monitor: Main.layoutManager.monitors[this._monitorIndex],
            rowSpacing,
            columnSpacing,
        });

        let lastLayout = null;
        let lastNumColumns = -1;
        let lastScale = 0;
        let lastSpace = 0;

        for (let numRows = 1; ; numRows++) {
            const numColumns = Math.ceil(this._sortedWindows.length / numRows);

            // If adding a new row does not change column count just stop
            // (for instance: 9 windows, with 3 rows -> 3 columns, 4 rows ->
            // 3 columns as well => just use 3 rows then)
            if (numColumns === lastNumColumns)
                break;

            const layout = this._layoutStrategy.computeLayout(this._sortedWindows, {
                numRows,
            });

            const [scale, space] = this._layoutStrategy.computeScaleAndSpace(layout, area);

            if (lastLayout && !this._isBetterScaleAndSpace(lastScale, lastSpace, scale, space))
                break;

            lastLayout = layout;
            lastNumColumns = numColumns;
            lastScale = scale;
            lastSpace = space;
        }

        return lastLayout;
    }

    _getWindowSlots(containerBox) {
        [, , containerBox] =
            this._adjustSpacingAndPadding(null, null, containerBox);

        const availArea = {
            x: parseInt(containerBox.x1),
            y: parseInt(containerBox.y1),
            width: parseInt(containerBox.get_width()),
            height: parseInt(containerBox.get_height()),
        };

        return this._layoutStrategy.computeWindowSlots(this._layout, availArea);
    }

    _getAdjustedWorkarea(container) {
        const workarea = this._workarea.copy();

        if (container instanceof St.Widget) {
            const themeNode = container.get_theme_node();
            workarea.width -= themeNode.get_horizontal_padding();
            workarea.height -= themeNode.get_vertical_padding();
        }

        return workarea;
    }

    _syncWorkareaTracking() {
        if (this._container) {
            if (this._workAreaChangedId)
                return;
            this._workarea = Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex);
            this._workareasChangedId =
                global.display.connect('workareas-changed', () => {
                    this._workarea = Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex);
                    this.layout_changed();
                });
        } else if (this._workareasChangedId) {
            global.display.disconnect(this._workareasChangedId);
            this._workareasChangedId = 0;
        }
    }

    vfunc_set_container(container) {
        this._container = container;
        this._syncWorkareaTracking();
        this._stateAdjustment.actor = container;
    }

    vfunc_get_preferred_width(container, forHeight) {
        const workarea = this._getAdjustedWorkarea(container);
        if (forHeight === -1)
            return [0, workarea.width];

        const workAreaAspectRatio = workarea.width / workarea.height;
        const widthPreservingAspectRatio = forHeight * workAreaAspectRatio;

        return [0, widthPreservingAspectRatio];
    }

    vfunc_get_preferred_height(container, forWidth) {
        const workarea = this._getAdjustedWorkarea(container);
        if (forWidth === -1)
            return [0, workarea.height];

        const workAreaAspectRatio = workarea.width / workarea.height;
        const heightPreservingAspectRatio = forWidth / workAreaAspectRatio;

        return [0, heightPreservingAspectRatio];
    }

    vfunc_allocate(container, box) {
        const containerBox = container.allocation;
        const [containerWidth, containerHeight] = containerBox.get_size();
        const containerAllocationChanged =
            this._lastBox === null || !this._lastBox.equal(containerBox);

        // If the containers size changed, we can no longer keep around
        // the old windowSlots, so we must unfreeze the layout.
        //
        // However, if the overview animation is in progress, don't unfreeze
        // the layout. This is needed to prevent windows "snapping" to their
        // new positions during the overview closing animation when the
        // allocation subtly expands every frame.
        if (this._layoutFrozen && containerAllocationChanged && !Main.overview.animationInProgress) {
            this._layoutFrozen = false;
            this.notify('layout-frozen');
        }

        const {ControlsState} = OverviewControls;
        const {currentState} =
            this._overviewAdjustment.getStateTransitionParams();
        const inSessionTransition = currentState <= ControlsState.WINDOW_PICKER;

        const window = this._sortedWindows[0];

        if (inSessionTransition || !window) {
            container.remove_clip();
        } else {
            const [, bottomOversize] = window.chromeHeights();
            const [containerX, containerY] = containerBox.get_origin();

            const extraHeightProgress =
                currentState - OverviewControls.ControlsState.WINDOW_PICKER;

            const extraClipHeight = bottomOversize * (1 - extraHeightProgress);

            container.set_clip(containerX, containerY,
                containerWidth, containerHeight + extraClipHeight);
        }

        let layoutChanged = false;
        if (!this._layoutFrozen || !this._lastBox) {
            if (this._needsLayout) {
                this._layout = this._createBestLayout(this._workarea);
                this._needsLayout = false;
                layoutChanged = true;
            }

            if (layoutChanged || containerAllocationChanged) {
                this._windowSlotsBox = box.copy();
                this._windowSlots = this._getWindowSlots(this._windowSlotsBox);
            }
        }

        const slotsScale = box.get_width() / this._windowSlotsBox.get_width();
        const workareaX = this._workarea.x;
        const workareaY = this._workarea.y;
        const workareaWidth = this._workarea.width;
        const stateAdjustementValue = this._stateAdjustment.value;

        const allocationScale = containerWidth / workareaWidth;

        const childBox = new Clutter.ActorBox();

        const nSlots = this._windowSlots.length;
        for (let i = 0; i < nSlots; i++) {
            let [x, y, width, height, child] = this._windowSlots[i];
            if (!child.visible)
                continue;

            x *= slotsScale;
            y *= slotsScale;
            width *= slotsScale;
            height *= slotsScale;

            const windowInfo = this._windows.get(child);

            let workspaceBoxX, workspaceBoxY;
            let workspaceBoxWidth, workspaceBoxHeight;

            if (windowInfo.metaWindow.showing_on_its_workspace()) {
                workspaceBoxX = (child.boundingBox.x - workareaX) * allocationScale;
                workspaceBoxY = (child.boundingBox.y - workareaY) * allocationScale;
                workspaceBoxWidth = child.boundingBox.width * allocationScale;
                workspaceBoxHeight = child.boundingBox.height * allocationScale;
            } else {
                workspaceBoxX = workareaX * allocationScale;
                workspaceBoxY = workareaY * allocationScale;
                workspaceBoxWidth = 0;
                workspaceBoxHeight = 0;
            }

            // Don't allow the scaled floating size to drop below
            // the target layout size.
            // We only want to apply this when the scaled floating size is
            // actually larger than the target layout size, that is while
            // animating between the session and the window picker.
            if (inSessionTransition) {
                workspaceBoxWidth = Math.max(workspaceBoxWidth, width);
                workspaceBoxHeight = Math.max(workspaceBoxHeight, height);
            }

            x = Util.lerp(workspaceBoxX, x, stateAdjustementValue);
            y = Util.lerp(workspaceBoxY, y, stateAdjustementValue);
            width = Util.lerp(workspaceBoxWidth, width, stateAdjustementValue);
            height = Util.lerp(workspaceBoxHeight, height, stateAdjustementValue);

            childBox.set_origin(x, y);
            childBox.set_size(width, height);

            if (windowInfo.currentTransition) {
                windowInfo.currentTransition.get_interval().set_final(childBox);

                // The timeline of the transition might not have been updated
                // before this allocation cycle, so make sure the child
                // still updates needs_allocation to FALSE.
                // Unfortunately, this relies on the fast paths in
                // clutter_actor_allocate(), otherwise we'd start a new
                // transition on the child, replacing the current one.
                child.allocate(child.allocation);
                continue;
            }

            // We want layout changes (ie. larger changes to the layout like
            // reshuffling the window order) to be animated, but small changes
            // like changes to the container size to happen immediately (for
            // example if the container height is being animated, we want to
            // avoid animating the children allocations to make sure they
            // don't "lag behind" the other animation).
            if (layoutChanged && !Main.overview.animationInProgress) {
                const transition = animateAllocation(child, childBox);
                if (transition) {
                    windowInfo.currentTransition = transition;
                    windowInfo.currentTransition.connect('stopped', () => {
                        windowInfo.currentTransition = null;
                    });
                }
            } else {
                child.allocate(childBox);
            }
        }

        this._lastBox = containerBox.copy();
    }

    _syncOverlay(preview) {
        const active = this._metaWorkspace?.active ?? true;
        preview.overlayEnabled = active && this._stateAdjustment.value === 1;
    }

    /**
     * Synchronizes the overlay state of all window previews.
     */
    syncOverlays() {
        [...this._windows.keys()].forEach(preview => this._syncOverlay(preview));
    }

    /**
     * Adds `window` to the workspace, it will be shown immediately if
     * the layout isn't frozen using the layout-frozen property.
     *
     * If `window` is already part of the workspace, nothing will happen.
     *
     * @param {WindowPreview} window - the window to add
     * @param {Meta.Window} metaWindow - the MetaWindow of the window
     */
    addWindow(window, metaWindow) {
        if (this._windows.has(window))
            return;

        this._windows.set(window, {
            metaWindow,
            sizeChangedId: metaWindow.connect('size-changed', () => {
                this._needsLayout = true;
                this.layout_changed();
            }),
            destroyId: window.connect('destroy', () =>
                this.removeWindow(window)),
            currentTransition: null,
        });

        this._sortedWindows.push(window);
        this._sortedWindows.sort((a, b) => {
            const winA = this._windows.get(a).metaWindow;
            const winB = this._windows.get(b).metaWindow;

            return winA.get_stable_sequence() - winB.get_stable_sequence();
        });

        this._syncOpacity(window, metaWindow);
        this._syncOverlay(window);
        this._container.add_child(window);

        this._needsLayout = true;
        this.layout_changed();
    }

    /**
     * Removes `window` from the workspace if `window` is a part of the
     * workspace. If the layout-frozen property is set to true, the
     * window will still be visible until the property is set to false.
     *
     * @param {WindowPreview} window - the window to remove
     */
    removeWindow(window) {
        const windowInfo = this._windows.get(window);
        if (!windowInfo)
            return;

        windowInfo.metaWindow.disconnect(windowInfo.sizeChangedId);
        window.disconnect(windowInfo.destroyId);
        if (windowInfo.currentTransition)
            window.remove_transition('allocation');

        this._windows.delete(window);
        this._sortedWindows.splice(this._sortedWindows.indexOf(window), 1);

        // The layout might be frozen and we might not update the windowSlots
        // on the next allocation, so remove the slot now already
        const index = this._windowSlots.findIndex(s => s[4] === window);
        if (index !== -1)
            this._windowSlots.splice(index, 1);

        // The window might have been reparented by DND
        if (window.get_parent() === this._container)
            this._container.remove_child(window);

        this._needsLayout = true;
        this.layout_changed();
    }

    syncStacking(stackIndices) {
        const windows = [...this._windows.keys()];
        windows.sort((a, b) => {
            const seqA = this._windows.get(a).metaWindow.get_stable_sequence();
            const seqB = this._windows.get(b).metaWindow.get_stable_sequence();

            return stackIndices[seqA] - stackIndices[seqB];
        });

        let lastWindow = null;
        for (const window of windows) {
            window.setStackAbove(lastWindow);
            lastWindow = window;
        }

        this._needsLayout = true;
        this.layout_changed();
    }

    /**
     * getFocusChain:
     *
     * Gets the focus chain of the workspace. This function will return
     * an empty array if the floating window layout is used.
     *
     * @returns {Array} an array of {Clutter.Actor}s
     */
    getFocusChain() {
        if (this._stateAdjustment.value === 0)
            return [];

        // The fifth element in the slot array is the WindowPreview
        return this._windowSlots.map(s => s[4]);
    }

    /**
     * An StAdjustment for controlling and transitioning between
     * the alignment of windows using the layout strategy and the
     * floating window layout.
     *
     * A value of 0 of the adjustment completely uses the floating
     * window layout while a value of 1 completely aligns windows using
     * the layout strategy.
     *
     * @type {St.Adjustment}
     */
    get stateAdjustment() {
        return this._stateAdjustment;
    }

    get spacing() {
        return this._spacing;
    }

    set spacing(s) {
        if (this._spacing === s)
            return;

        this._spacing = s;

        this._needsLayout = true;
        this.notify('spacing');
        this.layout_changed();
    }

    get layoutFrozen() {
        return this._layoutFrozen;
    }

    set layoutFrozen(f) {
        if (this._layoutFrozen === f)
            return;

        this._layoutFrozen = f;

        this.notify('layout-frozen');
        if (!this._layoutFrozen)
            this.layout_changed();
    }
});

export const WorkspaceBackground = GObject.registerClass(
class WorkspaceBackground extends Shell.WorkspaceBackground {
    _init(monitorIndex, stateAdjustment) {
        super._init({
            style_class: 'workspace-background',
            x_expand: true,
            y_expand: true,
            monitor_index: monitorIndex,
        });

        this._monitorIndex = monitorIndex;
        this._workarea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);

        this._stateAdjustment = stateAdjustment;
        this._stateAdjustment.connectObject('notify::value', () => {
            this._updateBorderRadius();
            this.queue_relayout();
        }, this);
        this._stateAdjustment.bind_property(
            'value', this, 'state-adjustment-value',
            GObject.BindingFlags.SYNC_CREATE
        );

        this._bin = new Clutter.Actor({
            layout_manager: new Clutter.BinLayout(),
            clip_to_allocation: true,
        });

        this._backgroundGroup = new Meta.BackgroundGroup({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_expand: true,
        });
        this._bin.add_child(this._backgroundGroup);
        this.add_child(this._bin);

        this._bgManager = new Background.BackgroundManager({
            container: this._backgroundGroup,
            monitorIndex: this._monitorIndex,
            controlPosition: false,
            useContentSize: false,
        });

        this._bgManager.connect('changed', () => {
            this._updateRoundedClipBounds();
            this._updateBorderRadius();
        });

        global.display.connectObject('workareas-changed', () => {
            this._workarea = Main.layoutManager.getWorkAreaForMonitor(monitorIndex);
            this._updateRoundedClipBounds();
            this.queue_relayout();
        }, this);
        this._updateRoundedClipBounds();

        this._updateBorderRadius();

        this.connect('destroy', this._onDestroy.bind(this));
    }

    _updateBorderRadius() {
        const {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
        const cornerRadius = scaleFactor * BACKGROUND_CORNER_RADIUS_PIXELS;

        const backgroundContent = this._bgManager.backgroundActor.content;
        backgroundContent.rounded_clip_radius =
            Util.lerp(0, cornerRadius, this._stateAdjustment.value);
    }

    _updateRoundedClipBounds() {
        const monitor = Main.layoutManager.monitors[this._monitorIndex];

        const rect = new Graphene.Rect();
        rect.origin.x = this._workarea.x - monitor.x;
        rect.origin.y = this._workarea.y - monitor.y;
        rect.size.width = this._workarea.width;
        rect.size.height = this._workarea.height;

        this._bgManager.backgroundActor.content.set_rounded_clip_bounds(rect);
    }

    _onDestroy() {
        if (this._bgManager) {
            this._bgManager.destroy();
            this._bgManager = null;
        }
    }
});

export const Workspace = GObject.registerClass(
class Workspace extends St.Widget {
    /**
     * @param {Meta.Workspace} metaWorkspace
     * @param {number} monitorIndex
     * @param {OverviewAdjustment} overviewAdjustment
     */
    _init(metaWorkspace, monitorIndex, overviewAdjustment) {
        super._init({
            style_class: 'window-picker',
            pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
            layout_manager: new Clutter.BinLayout(),
        });

        const layoutManager = new WorkspaceLayout(metaWorkspace, monitorIndex,
            overviewAdjustment);

        // Background
        this._background =
            new WorkspaceBackground(monitorIndex, layoutManager.stateAdjustment);
        this.add_child(this._background);

        // Window previews
        this._container = new Clutter.Actor({
            reactive: true,
            x_expand: true,
            y_expand: true,
        });
        this._container.layout_manager = layoutManager;
        this.add_child(this._container);

        this.metaWorkspace = metaWorkspace;

        this._overviewAdjustment = overviewAdjustment;

        this.monitorIndex = monitorIndex;
        this._monitor = Main.layoutManager.monitors[this.monitorIndex];

        if (monitorIndex !== Main.layoutManager.primaryIndex)
            this.add_style_class_name('external-monitor');

        const clickAction = new Clutter.ClickAction();
        clickAction.connect('clicked', action => {
            // Switch to the workspace when not the active one, leave the
            // overview otherwise.
            if (action.get_button() === 1 || action.get_button() === 0) {
                const leaveOverview = this._shouldLeaveOverview();

                this.metaWorkspace?.activate(global.get_current_time());
                if (leaveOverview)
                    Main.overview.hide();
            }
        });
        this.bind_property('mapped', clickAction, 'enabled', GObject.BindingFlags.SYNC_CREATE);
        this._container.add_action(clickAction);

        this.connect('style-changed', this._onStyleChanged.bind(this));
        this.connect('destroy', this._onDestroy.bind(this));

        this._skipTaskbarSignals = new Map();
        const windows = global.get_window_actors().map(a => a.meta_window)
            .filter(this._isMyWindow, this);

        // Create clones for windows that should be
        // visible in the Overview
        this._windows = [];
        for (let i = 0; i < windows.length; i++) {
            if (this._isOverviewWindow(windows[i]))
                this._addWindowClone(windows[i]);
        }

        // Track window changes, but let the window tracker process them first
        this.metaWorkspace?.connectObject(
            'window-added', this._windowAdded.bind(this), GObject.ConnectFlags.AFTER,
            'window-removed', this._windowRemoved.bind(this), GObject.ConnectFlags.AFTER,
            'notify::active', () => layoutManager.syncOverlays(), this);
        global.display.connectObject(
            'window-entered-monitor', this._windowEnteredMonitor.bind(this), GObject.ConnectFlags.AFTER,
            'window-left-monitor', this._windowLeftMonitor.bind(this), GObject.ConnectFlags.AFTER,
            this);
        this._layoutFrozenId = 0;

        // DND requires this to be set
        this._delegate = this;
    }

    _shouldLeaveOverview() {
        if (!this.metaWorkspace || this.metaWorkspace.active)
            return true;

        const overviewState = this._overviewAdjustment.value;
        return overviewState > OverviewControls.ControlsState.WINDOW_PICKER;
    }

    vfunc_get_focus_chain() {
        return this._container.layout_manager.getFocusChain();
    }

    _lookupIndex(metaWindow) {
        return this._windows.findIndex(w => w.metaWindow === metaWindow);
    }

    containsMetaWindow(metaWindow) {
        return this._lookupIndex(metaWindow) >= 0;
    }

    isEmpty() {
        return this._windows.length === 0;
    }

    syncStacking(stackIndices) {
        this._container.layout_manager.syncStacking(stackIndices);
    }

    _doRemoveWindow(metaWin) {
        let clone = this._removeWindowClone(metaWin);

        if (!clone)
            return;

        clone.destroy();

        // We need to reposition the windows; to avoid shuffling windows
        // around while the user is interacting with the workspace, we delay
        // the positioning until the pointer remains still for at least 750 ms
        // or is moved outside the workspace
        this._container.layout_manager.layout_frozen = true;

        if (this._layoutFrozenId > 0) {
            GLib.source_remove(this._layoutFrozenId);
            this._layoutFrozenId = 0;
        }

        let [oldX, oldY] = global.get_pointer();

        this._layoutFrozenId = GLib.timeout_add(
            GLib.PRIORITY_DEFAULT,
            WINDOW_REPOSITIONING_DELAY,
            () => {
                const [newX, newY] = global.get_pointer();
                const pointerHasMoved = oldX !== newX || oldY !== newY;
                const actorUnderPointer = global.stage.get_actor_at_pos(
                    Clutter.PickMode.REACTIVE, newX, newY);

                if ((pointerHasMoved && this.contains(actorUnderPointer)) ||
                    this._windows.some(w => w.contains(actorUnderPointer))) {
                    oldX = newX;
                    oldY = newY;
                    return GLib.SOURCE_CONTINUE;
                }

                this._container.layout_manager.layout_frozen = false;
                this._layoutFrozenId = 0;
                return GLib.SOURCE_REMOVE;
            });

        GLib.Source.set_name_by_id(this._layoutFrozenId,
            '[gnome-shell] this._layoutFrozenId');
    }

    _doAddWindow(metaWin) {
        let win = metaWin.get_compositor_private();

        if (!win) {
            // Newly-created windows are added to a workspace before
            // the compositor finds out about them...
            let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
                if (metaWin.get_compositor_private() &&
                    metaWin.get_workspace() === this.metaWorkspace)
                    this._doAddWindow(metaWin);
                return GLib.SOURCE_REMOVE;
            });
            GLib.Source.set_name_by_id(id, '[gnome-shell] this._doAddWindow');
            return;
        }

        // We might have the window in our list already if it was on all workspaces and
        // now was moved to this workspace
        if (this._lookupIndex(metaWin) !== -1)
            return;

        if (!this._isMyWindow(metaWin))
            return;

        this._skipTaskbarSignals.set(metaWin,
            metaWin.connect('notify::skip-taskbar', () => {
                if (metaWin.skip_taskbar)
                    this._doRemoveWindow(metaWin);
                else
                    this._doAddWindow(metaWin);
            }));

        if (!this._isOverviewWindow(metaWin)) {
            if (metaWin.get_transient_for() == null)
                return;

            // Let the top-most ancestor handle all transients
            let parent = metaWin.find_root_ancestor();
            let clone = this._windows.find(c => c.metaWindow === parent);

            // If no clone was found, the parent hasn't been created yet
            // and will take care of the dialog when added
            if (clone)
                clone.addDialog(metaWin);

            return;
        }

        const clone = this._addWindowClone(metaWin);

        clone.set_pivot_point(0.5, 0.5);
        clone.scale_x = 0;
        clone.scale_y = 0;
        clone.ease({
            scale_x: 1,
            scale_y: 1,
            duration: 250,
            onStopped: () => clone.set_pivot_point(0, 0),
        });

        if (this._layoutFrozenId > 0) {
            // If a window was closed before, unfreeze the layout to ensure
            // the new window is immediately shown
            this._container.layout_manager.layout_frozen = false;

            GLib.source_remove(this._layoutFrozenId);
            this._layoutFrozenId = 0;
        }
    }

    _windowAdded(metaWorkspace, metaWin) {
        if (!Main.overview.closing)
            this._doAddWindow(metaWin);
    }

    _windowRemoved(metaWorkspace, metaWin) {
        this._doRemoveWindow(metaWin);
    }

    _windowEnteredMonitor(metaDisplay, monitorIndex, metaWin) {
        if (monitorIndex === this.monitorIndex && !Main.overview.closing)
            this._doAddWindow(metaWin);
    }

    _windowLeftMonitor(metaDisplay, monitorIndex, metaWin) {
        if (monitorIndex === this.monitorIndex)
            this._doRemoveWindow(metaWin);
    }

    // check for maximized windows on the workspace
    hasMaximizedWindows() {
        for (let i = 0; i < this._windows.length; i++) {
            let metaWindow = this._windows[i].metaWindow;
            if (metaWindow.showing_on_its_workspace() &&
                metaWindow.maximized_horizontally &&
                metaWindow.maximized_vertically)
                return true;
        }
        return false;
    }

    _clearSkipTaskbarSignals() {
        for (const [metaWin, id] of this._skipTaskbarSignals)
            metaWin.disconnect(id);
        this._skipTaskbarSignals.clear();
    }

    prepareToLeaveOverview() {
        this._clearSkipTaskbarSignals();

        for (let i = 0; i < this._windows.length; i++)
            this._windows[i].remove_all_transitions();

        if (this._layoutFrozenId > 0) {
            GLib.source_remove(this._layoutFrozenId);
            this._layoutFrozenId = 0;
        }

        this._container.layout_manager.layout_frozen = true;
        Main.overview.connectObject(
            'hidden', this._doneLeavingOverview.bind(this), this);
    }

    _onDestroy() {
        this._clearSkipTaskbarSignals();

        if (this._layoutFrozenId > 0) {
            GLib.source_remove(this._layoutFrozenId);
            this._layoutFrozenId = 0;
        }

        this._windows = [];
    }

    _doneLeavingOverview() {
        this._container.layout_manager.layout_frozen = false;
    }

    _doneShowingOverview() {
        this._container.layout_manager.layout_frozen = false;
    }

    _isMyWindow(window) {
        const isOnWorkspace = this.metaWorkspace === null ||
            window.located_on_workspace(this.metaWorkspace);
        const isOnMonitor = window.get_monitor() === this.monitorIndex;

        return isOnWorkspace && isOnMonitor;
    }

    _isOverviewWindow(window) {
        return !window.skip_taskbar;
    }

    // Create a clone of a (non-desktop) window and add it to the window list
    _addWindowClone(metaWindow) {
        let clone = new WindowPreview(metaWindow, this, this._overviewAdjustment);

        clone.connect('selected',
            this._onCloneSelected.bind(this));
        clone.connect('drag-begin', () => {
            Main.overview.beginWindowDrag(metaWindow);
        });
        clone.connect('drag-cancelled', () => {
            Main.overview.cancelledWindowDrag(metaWindow);
        });
        clone.connect('drag-end', () => {
            Main.overview.endWindowDrag(metaWindow);
        });
        clone.connect('show-chrome', () => {
            let focus = global.stage.key_focus;
            if (focus == null || this.contains(focus))
                clone.grab_key_focus();

            this._windows.forEach(c => {
                if (c !== clone)
                    c.hideOverlay(true);
            });
        });
        clone.connect('destroy', () => {
            this._doRemoveWindow(metaWindow);
        });

        this._container.layout_manager.addWindow(clone, metaWindow);

        if (this._windows.length === 0)
            clone.setStackAbove(null);
        else
            clone.setStackAbove(this._windows[this._windows.length - 1]);

        this._windows.push(clone);

        return clone;
    }

    _removeWindowClone(metaWin) {
        // find the position of the window in our list
        let index = this._lookupIndex(metaWin);

        if (index === -1)
            return null;

        this._container.layout_manager.removeWindow(this._windows[index]);

        return this._windows.splice(index, 1).pop();
    }

    _onStyleChanged() {
        const themeNode = this.get_theme_node();
        this._container.layout_manager.spacing = themeNode.get_length('spacing');
    }

    _onCloneSelected(clone, time) {
        const wsIndex = this.metaWorkspace?.index();

        if (this._shouldLeaveOverview())
            Main.activateWindow(clone.metaWindow, time, wsIndex);
        else
            this.metaWorkspace?.activate(time);
    }

    // Draggable target interface
    handleDragOver(source, _actor, _x, _y, _time) {
        if (source.metaWindow && !this._isMyWindow(source.metaWindow))
            return DND.DragMotionResult.MOVE_DROP;
        if (source.app && source.app.can_open_new_window())
            return DND.DragMotionResult.COPY_DROP;
        if (!source.app && source.shellWorkspaceLaunch)
            return DND.DragMotionResult.COPY_DROP;

        return DND.DragMotionResult.CONTINUE;
    }

    acceptDrop(source, actor, x, y, time) {
        let workspaceManager = global.workspace_manager;
        let workspaceIndex = this.metaWorkspace
            ? this.metaWorkspace.index()
            : workspaceManager.get_active_workspace_index();

        if (source.metaWindow) {
            const window = source.metaWindow;
            if (this._isMyWindow(window))
                return false;

            Main.moveWindowToMonitorAndWorkspace(window,
                this.monitorIndex, workspaceIndex);
            return true;
        } else if (source.app && source.app.can_open_new_window()) {
            if (source.animateLaunchAtPos)
                source.animateLaunchAtPos(actor.x, actor.y);

            source.app.open_new_window(workspaceIndex);
            return true;
        } else if (!source.app && source.shellWorkspaceLaunch) {
            // While unused in our own drag sources, shellWorkspaceLaunch allows
            // extensions to define custom actions for their drag sources.
            source.shellWorkspaceLaunch({
                workspace: workspaceIndex,
                timestamp: time,
            });
            return true;
        }

        return false;
    }

    get stateAdjustment() {
        return this._container.layout_manager.stateAdjustment;
    }
});
(uuay)vmware.js // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import * as Credential from './credentialManager.js';

const dbusPath = '/org/vmware/viewagent/Credentials';
const dbusInterface = 'org.vmware.viewagent.Credentials';

export const SERVICE_NAME = 'gdm-vmwcred';

const VmwareCredentialsIface = `<node>
<interface name="${dbusInterface}">
<signal name="UserAuthenticated">
    <arg type="s" name="token"/>
</signal>
</interface>
</node>`;


const VmwareCredentialsInfo = Gio.DBusInterfaceInfo.new_for_xml(VmwareCredentialsIface);

let _vmwareCredentialsManager = null;

function VmwareCredentials() {
    var self = new Gio.DBusProxy({
        g_connection: Gio.DBus.session,
        g_interface_name: VmwareCredentialsInfo.name,
        g_interface_info: VmwareCredentialsInfo,
        g_name: dbusInterface,
        g_object_path: dbusPath,
        g_flags: Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
    });
    self.init(null);
    return self;
}

class VmwareCredentialsManager extends Credential.CredentialManager {
    constructor() {
        super(SERVICE_NAME);
        this._credentials = new VmwareCredentials();
        this._credentials.connectSignal('UserAuthenticated',
            (proxy, sender, [token]) => {
                this.token = token;
            });
    }
}

/**
 * @returns {VmwareCredentialsManager}
 */
export function getVmwareCredentialsManager() {
    if (!_vmwareCredentialsManager)
        _vmwareCredentialsManager = new VmwareCredentialsManager();

    return _vmwareCredentialsManager;
}
(uuay)automountManager.js)$// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import * as Params from '../../misc/params.js';

import * as GnomeSession from '../../misc/gnomeSession.js';
import * as Main from '../main.js';
import * as ShellMountOperation from '../shellMountOperation.js';

const GNOME_SESSION_AUTOMOUNT_INHIBIT = 16;

// GSettings keys
const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
const SETTING_ENABLE_AUTOMOUNT = 'automount';

const AUTORUN_EXPIRE_TIMEOUT_SECS = 10;

class AutomountManager {
    constructor() {
        this._settings = new Gio.Settings({schema_id: SETTINGS_SCHEMA});
        this._activeOperations = new Map();
        this._session = new GnomeSession.SessionManager();
        this._session.connectSignal('InhibitorAdded',
            this._InhibitorsChanged.bind(this));
        this._session.connectSignal('InhibitorRemoved',
            this._InhibitorsChanged.bind(this));
        this._inhibited = false;

        this._volumeMonitor = Gio.VolumeMonitor.get();
    }

    enable() {
        this._volumeMonitor.connectObject(
            'volume-added', this._onVolumeAdded.bind(this),
            'volume-removed', this._onVolumeRemoved.bind(this),
            'drive-connected', this._onDriveConnected.bind(this),
            'drive-disconnected', this._onDriveDisconnected.bind(this),
            'drive-eject-button', this._onDriveEjectButton.bind(this), this);

        this._mountAllId = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._startupMountAll.bind(this));
        GLib.Source.set_name_by_id(this._mountAllId, '[gnome-shell] this._startupMountAll');
    }

    disable() {
        this._volumeMonitor.disconnectObject(this);

        if (this._mountAllId > 0) {
            GLib.source_remove(this._mountAllId);
            this._mountAllId = 0;
        }
    }

    async _InhibitorsChanged(_object, _senderName, [_inhibitor]) {
        try {
            const [inhibited] =
                await this._session.IsInhibitedAsync(GNOME_SESSION_AUTOMOUNT_INHIBIT);
            this._inhibited = inhibited;
        } catch (e) {}
    }

    _startupMountAll() {
        let volumes = this._volumeMonitor.get_volumes();
        volumes.forEach(volume => {
            this._checkAndMountVolume(volume, {
                checkSession: false,
                useMountOp: false,
                allowAutorun: false,
            });
        });

        this._mountAllId = 0;
        return GLib.SOURCE_REMOVE;
    }

    _onDriveConnected() {
        // if we're not in the current ConsoleKit session,
        // or screensaver is active, don't play sounds
        if (!this._session.SessionIsActive)
            return;

        let player = global.display.get_sound_player();
        player.play_from_theme('device-added-media',
            _('External drive connected'),
            null);
    }

    _onDriveDisconnected() {
        // if we're not in the current ConsoleKit session,
        // or screensaver is active, don't play sounds
        if (!this._session.SessionIsActive)
            return;

        let player = global.display.get_sound_player();
        player.play_from_theme('device-removed-media',
            _('External drive disconnected'),
            null);
    }

    _onDriveEjectButton(monitor, drive) {
        // TODO: this code path is not tested, as the GVfs volume monitor
        // doesn't emit this signal just yet.
        if (!this._session.SessionIsActive)
            return;

        // we force stop/eject in this case, so we don't have to pass a
        // mount operation object
        if (drive.can_stop()) {
            drive.stop(Gio.MountUnmountFlags.FORCE, null, null,
                (o, res) => {
                    try {
                        drive.stop_finish(res);
                    } catch (e) {
                        log(`Unable to stop the drive after drive-eject-button ${e.toString()}`);
                    }
                });
        } else if (drive.can_eject()) {
            drive.eject_with_operation(Gio.MountUnmountFlags.FORCE, null, null,
                (o, res) => {
                    try {
                        drive.eject_with_operation_finish(res);
                    } catch (e) {
                        log(`Unable to eject the drive after drive-eject-button ${e.toString()}`);
                    }
                });
        }
    }

    _onVolumeAdded(monitor, volume) {
        this._checkAndMountVolume(volume);
    }

    _checkAndMountVolume(volume, params) {
        params = Params.parse(params, {
            checkSession: true,
            useMountOp: true,
            allowAutorun: true,
        });

        if (params.checkSession) {
            // if we're not in the current ConsoleKit session,
            // don't attempt automount
            if (!this._session.SessionIsActive)
                return;
        }

        if (this._inhibited)
            return;

        // Volume is already mounted, don't bother.
        if (volume.get_mount())
            return;

        if (!this._settings.get_boolean(SETTING_ENABLE_AUTOMOUNT) ||
            !volume.should_automount() ||
            !volume.can_mount()) {
            // allow the autorun to run anyway; this can happen if the
            // mount gets added programmatically later, even if
            // should_automount() or can_mount() are false, like for
            // blank optical media.
            this._allowAutorun(volume);
            this._allowAutorunExpire(volume);

            return;
        }

        if (params.useMountOp) {
            let operation = new ShellMountOperation.ShellMountOperation(volume);
            this._mountVolume(volume, operation, params.allowAutorun);
        } else {
            this._mountVolume(volume, null, params.allowAutorun);
        }
    }

    _mountVolume(volume, operation, allowAutorun) {
        if (allowAutorun)
            this._allowAutorun(volume);

        const mountOp = operation?.mountOp ?? null;
        this._activeOperations.set(volume, operation);

        volume.mount(0, mountOp, null,
            this._onVolumeMounted.bind(this));
    }

    _onVolumeMounted(volume, res) {
        this._allowAutorunExpire(volume);

        try {
            volume.mount_finish(res);
            this._closeOperation(volume);
        } catch (e) {
            // FIXME: we will always get G_IO_ERROR_FAILED from the gvfs udisks
            // backend, see https://bugs.freedesktop.org/show_bug.cgi?id=51271
            // To reask the password if the user input was empty or wrong, we
            // will check for corresponding error messages. However, these
            // error strings are not unique for the cases in the comments below.
            if (e.message.includes('No key available with this passphrase') || // cryptsetup
                e.message.includes('No key available to unlock device') ||     // udisks (no password)
                // libblockdev wrong password opening LUKS device
                e.message.includes('Failed to activate device: Incorrect passphrase') ||
                // cryptsetup returns EINVAL in many cases, including wrong TCRYPT password/parameters
                e.message.includes('Failed to load device\'s parameters: Invalid argument')) {
                this._reaskPassword(volume);
            } else {
                if (e.message.includes('Compiled against a version of libcryptsetup that does not support the VeraCrypt PIM setting')) {
                    Main.notifyError(_('Unable to unlock volume'),
                        _('The installed udisks version does not support the PIM setting'));
                }

                if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
                    log(`Unable to mount volume ${volume.get_name()}: ${e.toString()}`);
                this._closeOperation(volume);
            }
        }
    }

    _onVolumeRemoved(monitor, volume) {
        if (volume._allowAutorunExpireId && volume._allowAutorunExpireId > 0) {
            GLib.source_remove(volume._allowAutorunExpireId);
            delete volume._allowAutorunExpireId;
        }
    }

    _reaskPassword(volume) {
        let prevOperation = this._activeOperations.get(volume);
        const existingDialog = prevOperation?.borrowDialog();
        let operation =
            new ShellMountOperation.ShellMountOperation(volume, {existingDialog});
        this._mountVolume(volume, operation);
    }

    _closeOperation(volume) {
        let operation = this._activeOperations.get(volume);
        if (!operation)
            return;
        operation.close();
        this._activeOperations.delete(volume);
    }

    _allowAutorun(volume) {
        volume.allowAutorun = true;
    }

    _allowAutorunExpire(volume) {
        let id = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, AUTORUN_EXPIRE_TIMEOUT_SECS, () => {
            volume.allowAutorun = false;
            delete volume._allowAutorunExpireId;
            return GLib.SOURCE_REMOVE;
        });
        volume._allowAutorunExpireId = id;
        GLib.Source.set_name_by_id(id, '[gnome-shell] volume.allowAutorun');
    }
}

export {AutomountManager as Component};
(uuay)barLevel.js-/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import St from 'gi://St';

export const BarLevel = GObject.registerClass({
    Properties: {
        'value': GObject.ParamSpec.double(
            'value', 'value', 'value',
            GObject.ParamFlags.READWRITE,
            0, 2, 0),
        'maximum-value': GObject.ParamSpec.double(
            'maximum-value', 'maximum-value', 'maximum-value',
            GObject.ParamFlags.READWRITE,
            1, 2, 1),
        'overdrive-start': GObject.ParamSpec.double(
            'overdrive-start', 'overdrive-start', 'overdrive-start',
            GObject.ParamFlags.READWRITE,
            1, 2, 1),
    },
}, class BarLevel extends St.DrawingArea {
    _init(params) {
        this._maxValue = 1;
        this._value = 0;
        this._overdriveStart = 1;
        this._barLevelWidth = 0;
        this._barLevelHeight = 0;

        this._barLevelBorderWidth = 0;
        this._overdriveSeparatorWidth = 0;

        this._barLevelColor = null;
        this._barLevelActiveColor = null;
        this._barLevelOverdriveColor = null;
        this._barLevelBorderColor = null;
        this._barLevelActiveBorderColor = null;
        this._barLevelOverdriveBorderColor = null;

        super._init({
            style_class: 'barlevel',
            accessible_role: Atk.Role.LEVEL_BAR,
            ...params,
        });
        this.connect('notify::allocation', () => {
            this._barLevelWidth = this.allocation.get_width();
        });

        this._customAccessible = St.GenericAccessible.new_for_actor(this);
        this.set_accessible(this._customAccessible);

        this._customAccessible.connect('get-current-value', this._getCurrentValue.bind(this));
        this._customAccessible.connect('get-minimum-value', this._getMinimumValue.bind(this));
        this._customAccessible.connect('get-maximum-value', this._getMaximumValue.bind(this));
        this._customAccessible.connect('set-current-value', this._setCurrentValue.bind(this));

        this.connect('notify::value', this._valueChanged.bind(this));
    }

    get value() {
        return this._value;
    }

    set value(value) {
        value = Math.max(Math.min(value, this._maxValue), 0);

        if (this._value === value)
            return;

        this._value = value;
        this.notify('value');
        this.queue_repaint();
    }

    get maximumValue() {
        return this._maxValue;
    }

    set maximumValue(value) {
        value = Math.max(value, 1);

        if (this._maxValue === value)
            return;

        this._maxValue = value;
        this._overdriveStart = Math.min(this._overdriveStart, this._maxValue);
        this.notify('maximum-value');
        this.queue_repaint();
    }

    get overdriveStart() {
        return this._overdriveStart;
    }

    set overdriveStart(value) {
        if (this._overdriveStart === value)
            return;

        if (value > this._maxValue) {
            throw new Error(`Tried to set overdrive value to ${value}, ` +
                `which is a number greater than the maximum allowed value ${this._maxValue}`);
        }

        this._overdriveStart = value;
        this.notify('overdrive-start');
        this.queue_repaint();
    }

    vfunc_style_changed() {
        super.vfunc_style_changed();

        const themeNode = this.get_theme_node();
        this._barLevelHeight = themeNode.get_length('-barlevel-height');
        this._barLevelBorderWidth =
            Math.min(themeNode.get_length('-barlevel-border-width'), this._barLevelHeight);
        this._overdriveSeparatorWidth =
            themeNode.get_length('-barlevel-overdrive-separator-width');

        this._barLevelColor = themeNode.get_color('-barlevel-background-color');
        this._barLevelActiveColor = themeNode.get_color('-barlevel-active-background-color');
        this._barLevelOverdriveColor = themeNode.get_color('-barlevel-overdrive-color');

        const [hasBorderColor, barLevelBorderColor] =
            themeNode.lookup_color('-barlevel-border-color', false);
        this._barLevelBorderColor = hasBorderColor
            ? barLevelBorderColor : this._barLevelColor;

        const [hasActiveBorderColor, barLevelActiveBorderColor] =
            themeNode.lookup_color('-barlevel-active-border-color', false);
        this._barLevelActiveBorderColor = hasActiveBorderColor
            ? barLevelActiveBorderColor : this._barLevelActiveColor;

        const [hasOverdriveBorderColor, barLevelOverdriveBorderColor] =
            themeNode.lookup_color('-barlevel-overdrive-border-color', false);
        this._barLevelOverdriveBorderColor = hasOverdriveBorderColor
            ? barLevelOverdriveBorderColor : this._barLevelOverdriveColor;
    }

    vfunc_repaint() {
        let cr = this.get_context();
        let themeNode = this.get_theme_node();
        let [width, height] = this.get_surface_size();
        const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;

        const barLevelBorderRadius = Math.min(width, this._barLevelHeight) / 2;
        let fgColor = themeNode.get_foreground_color();

        const TAU = Math.PI * 2;

        let endX = 0;
        if (this._maxValue > 0) {
            let progress = this._value / this._maxValue;
            if (rtl)
                progress = 1 - progress;
            endX = barLevelBorderRadius + (width - 2 * barLevelBorderRadius) * progress;
        }

        let overdriveRatio = this._overdriveStart / this._maxValue;
        if (rtl)
            overdriveRatio = 1 - overdriveRatio;
        let overdriveSeparatorX = barLevelBorderRadius + (width - 2 * barLevelBorderRadius) * overdriveRatio;

        let overdriveActive = this._overdriveStart !== this._maxValue;
        const overdriveSeparatorWidth = overdriveActive
            ? this._overdriveSeparatorWidth : 0;

        let xcArcStart = barLevelBorderRadius + this._barLevelBorderWidth;
        let xcArcEnd = width - xcArcStart;
        if (rtl)
            [xcArcStart, xcArcEnd] = [xcArcEnd, xcArcStart];

        /* background bar */
        if (!rtl)
            cr.arc(xcArcEnd, height / 2, barLevelBorderRadius, TAU * (3 / 4), TAU * (1 / 4));
        else
            cr.arcNegative(xcArcEnd, height / 2, barLevelBorderRadius, TAU * (3 / 4), TAU * (1 / 4));
        cr.lineTo(endX, (height + this._barLevelHeight) / 2);
        cr.lineTo(endX, (height - this._barLevelHeight) / 2);
        cr.lineTo(xcArcEnd, (height - this._barLevelHeight) / 2);
        cr.setSourceColor(this._barLevelColor);
        cr.fillPreserve();
        cr.setSourceColor(this._barLevelBorderColor);
        cr.setLineWidth(this._barLevelBorderWidth);
        cr.stroke();

        /* normal progress bar */
        let x = 0;
        if (!rtl) {
            x = Math.min(endX, overdriveSeparatorX - overdriveSeparatorWidth / 2);
            cr.arc(xcArcStart, height / 2, barLevelBorderRadius, TAU * (1 / 4), TAU * (3 / 4));
        } else {
            x = Math.max(endX, overdriveSeparatorX + overdriveSeparatorWidth / 2);
            cr.arcNegative(xcArcStart, height / 2, barLevelBorderRadius, TAU * (1 / 4), TAU * (3 / 4));
        }
        cr.lineTo(x, (height - this._barLevelHeight) / 2);
        cr.lineTo(x, (height + this._barLevelHeight) / 2);
        cr.lineTo(xcArcStart, (height + this._barLevelHeight) / 2);
        if (this._value > 0)
            cr.setSourceColor(this._barLevelActiveColor);
        cr.fillPreserve();
        cr.setSourceColor(this._barLevelActiveBorderColor);
        cr.setLineWidth(this._barLevelBorderWidth);
        cr.stroke();

        /* overdrive progress barLevel */
        if (!rtl)
            x = Math.min(endX, overdriveSeparatorX) + overdriveSeparatorWidth / 2;
        else
            x = Math.max(endX, overdriveSeparatorX) - overdriveSeparatorWidth / 2;
        if (this._value > this._overdriveStart) {
            cr.moveTo(x, (height - this._barLevelHeight) / 2);
            cr.lineTo(endX, (height - this._barLevelHeight) / 2);
            cr.lineTo(endX, (height + this._barLevelHeight) / 2);
            cr.lineTo(x, (height + this._barLevelHeight) / 2);
            cr.lineTo(x, (height - this._barLevelHeight) / 2);
            cr.setSourceColor(this._barLevelOverdriveColor);
            cr.fillPreserve();
            cr.setSourceColor(this._barLevelOverdriveBorderColor);
            cr.setLineWidth(this._barLevelBorderWidth);
            cr.stroke();
        }

        /* end progress bar arc */
        if (this._value > 0) {
            if (this._value <= this._overdriveStart)
                cr.setSourceColor(this._barLevelActiveColor);
            else
                cr.setSourceColor(this._barLevelOverdriveColor);
            if (!rtl) {
                cr.arc(endX, height / 2, barLevelBorderRadius, TAU * (3 / 4), TAU * (1 / 4));
                cr.lineTo(Math.floor(endX), (height + this._barLevelHeight) / 2);
                cr.lineTo(Math.floor(endX), (height - this._barLevelHeight) / 2);
            } else {
                cr.arcNegative(endX, height / 2, barLevelBorderRadius, TAU * (3 / 4), TAU * (1 / 4));
                cr.lineTo(Math.ceil(endX), (height + this._barLevelHeight) / 2);
                cr.lineTo(Math.ceil(endX), (height - this._barLevelHeight) / 2);
            }
            cr.lineTo(endX, (height - this._barLevelHeight) / 2);
            cr.fillPreserve();
            cr.setLineWidth(this._barLevelBorderWidth);
            cr.stroke();
        }

        /* draw overdrive separator */
        if (overdriveActive) {
            cr.moveTo(overdriveSeparatorX - overdriveSeparatorWidth / 2, (height - this._barLevelHeight) / 2);
            cr.lineTo(overdriveSeparatorX + overdriveSeparatorWidth / 2, (height - this._barLevelHeight) / 2);
            cr.lineTo(overdriveSeparatorX + overdriveSeparatorWidth / 2, (height + this._barLevelHeight) / 2);
            cr.lineTo(overdriveSeparatorX - overdriveSeparatorWidth / 2, (height + this._barLevelHeight) / 2);
            cr.lineTo(overdriveSeparatorX - overdriveSeparatorWidth / 2, (height - this._barLevelHeight) / 2);
            if (this._value <= this._overdriveStart)
                cr.setSourceColor(fgColor);
            else
                cr.setSourceColor(this._barLevelColor);
            cr.fill();
        }

        cr.$dispose();
    }

    vfunc_get_preferred_height(_forWidth) {
        const themeNode = this.get_theme_node();
        const height = this._getPreferredHeight();
        return themeNode.adjust_preferred_height(height, height);
    }

    vfunc_get_preferred_width(_forHeight) {
        const themeNode = this.get_theme_node();
        const width = this._getPreferredWidth();
        return themeNode.adjust_preferred_width(width, width);
    }

    _getPreferredHeight() {
        return this._barLevelHeight + this._barLevelBorderWidth;
    }

    _getPreferredWidth() {
        return this._overdriveSeparatorWidth + this._barLevelBorderWidth;
    }

    _getCurrentValue() {
        return this._value;
    }

    _getOverdriveStart() {
        return this._overdriveStart;
    }

    _getMinimumValue() {
        return 0;
    }

    _getMaximumValue() {
        return this._maxValue;
    }

    _setCurrentValue(_actor, value) {
        this._value = value;
    }

    _valueChanged() {
        this._customAccessible.notify('accessible-value');
    }
});
(uuay)extensionDownloader.js�,// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Soup from 'gi://Soup';

import * as Config from '../misc/config.js';
import * as Desktop from '../misc/desktop.js';
import * as Dialog from './dialog.js';
import * as ExtensionUtils from '../misc/extensionUtils.js';
import * as FileUtils from '../misc/fileUtils.js';
import * as Main from './main.js';
import * as ModalDialog from './modalDialog.js';

import {ExtensionErrors, ExtensionError} from '../misc/dbusErrors.js';

Gio._promisify(Soup.Session.prototype, 'send_and_read_async');
Gio._promisify(Gio.OutputStream.prototype, 'write_bytes_async');
Gio._promisify(Gio.IOStream.prototype, 'close_async');
Gio._promisify(Gio.Subprocess.prototype, 'wait_check_async');

const REPOSITORY_URL_DOWNLOAD = 'https://extensions.gnome.org/download-extension/%s.shell-extension.zip';
const REPOSITORY_URL_INFO     = 'https://extensions.gnome.org/extension-info/';
const REPOSITORY_URL_UPDATE   = 'https://extensions.gnome.org/update-info/';

let _httpSession;

/**
 * @param {string} uuid - extension uuid
 * @param {Gio.DBusMethodInvocation} invocation - the caller
 * @returns {void}
 */
export async function installExtension(uuid, invocation) {
    if (!global.settings.get_boolean('allow-extension-installation')) {
        invocation.return_error_literal(
            ExtensionErrors, ExtensionError.NOT_ALLOWED,
            'Extension installation is not allowed');
        return;
    }

    const params = {
        uuid,
        shell_version: Config.PACKAGE_VERSION,
    };

    if (Desktop.is('ubuntu') && Main.extensionManager.isModeExtension(uuid)) {
        const msg = _("This is an extension enabled by your current mode, you can’t install manually any update in that session.");
        Main.notifyError(_("Can't install “%s”:").format(uuid), msg);
        invocation.return_dbus_error('org.gnome.Shell.ExtensionError', msg);
        return;
    }

    const message = Soup.Message.new_from_encoded_form('GET',
        REPOSITORY_URL_INFO,
        Soup.form_encode_hash(params));

    let info;
    try {
        const bytes = await _httpSession.send_and_read_async(
            message,
            GLib.PRIORITY_DEFAULT,
            null);
        checkResponse(message);
        const decoder = new TextDecoder();
        info = JSON.parse(decoder.decode(bytes.get_data()));
    } catch (e) {
        Main.extensionManager.logExtensionError(uuid, e);
        invocation.return_error_literal(
            ExtensionErrors, ExtensionError.INFO_DOWNLOAD_FAILED,
            e.message);
        return;
    }

    const dialog = new InstallExtensionDialog(uuid, info, invocation);
    dialog.open();
}

/**
 * @param {string} uuid
 */
export function uninstallExtension(uuid) {
    let extension = Main.extensionManager.lookup(uuid);
    if (!extension)
        return false;

    // Don't try to uninstall system extensions
    if (extension.type !== ExtensionUtils.ExtensionType.PER_USER)
        return false;

    if (!Main.extensionManager.unloadExtension(extension))
        return false;

    FileUtils.recursivelyDeleteDir(extension.dir, true);

    try {
        const updatesDir = Gio.File.new_for_path(GLib.build_filenamev(
            [global.userdatadir, 'extension-updates', extension.uuid]));
        FileUtils.recursivelyDeleteDir(updatesDir, true);
    } catch (e) {
        // not an error
    }

    return true;
}

/**
 * Check return status of reponse
 *
 * @param {Soup.Message} message - an http response
 * @returns {void}
 * @throws
 */
function checkResponse(message) {
    const {statusCode} = message;
    const phrase = Soup.Status.get_phrase(statusCode);
    if (statusCode !== Soup.Status.OK)
        throw new Error(`Unexpected response: ${phrase}`);
}

/**
 * @param {GLib.Bytes} bytes - archive data
 * @param {Gio.File} dir - target directory
 * @returns {void}
 */
async function extractExtensionArchive(bytes, dir) {
    if (!dir.query_exists(null))
        dir.make_directory_with_parents(null);

    const [file, stream] = Gio.File.new_tmp('XXXXXX.shell-extension.zip');
    await stream.output_stream.write_bytes_async(bytes,
        GLib.PRIORITY_DEFAULT, null);
    stream.close_async(GLib.PRIORITY_DEFAULT, null);

    const unzip = Gio.Subprocess.new(
        ['unzip', '-uod', dir.get_path(), '--', file.get_path()],
        Gio.SubprocessFlags.NONE);
    await unzip.wait_check_async(null);

    const schemasPath = dir.get_child('schemas');

    try {
        const info = await schemasPath.query_info_async(
            Gio.FILE_ATTRIBUTE_STANDARD_TYPE,
            Gio.FileQueryInfoFlags.NONE,
            GLib.PRIORITY_DEFAULT,
            null);

        if (info.get_file_type() !== Gio.FileType.DIRECTORY)
            throw new Error('schemas is not a directory');
    } catch (e) {
        if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
            console.warn(`Error while looking for schema for extension ${dir.get_basename()}: ${e.message}`);
        return;
    }

    const compileSchema = Gio.Subprocess.new(
        ['glib-compile-schemas', '--strict', schemasPath.get_path()],
        Gio.SubprocessFlags.NONE);

    try {
        await compileSchema.wait_check_async(null);
    } catch (e) {
        log(`Error while compiling schema for extension ${dir.get_basename()}: (${e.message})`);
    }
}

/**
 * @param {string} uuid - extension uuid
 * @returns {void}
 */
export async function downloadExtensionUpdate(uuid) {
    if (!Main.extensionManager.updatesSupported)
        return;

    const dir = Gio.File.new_for_path(
        GLib.build_filenamev([global.userdatadir, 'extension-updates', uuid]));

    const params = {shell_version: Config.PACKAGE_VERSION};
    const message = Soup.Message.new_from_encoded_form('GET',
        REPOSITORY_URL_DOWNLOAD.format(uuid),
        Soup.form_encode_hash(params));

    try {
        const bytes = await _httpSession.send_and_read_async(
            message,
            GLib.PRIORITY_DEFAULT,
            null);
        checkResponse(message);

        await extractExtensionArchive(bytes, dir);
        Main.extensionManager.notifyExtensionUpdate(uuid);
    } catch (e) {
        log(`Error while downloading update for extension ${uuid}: (${e.message})`);
    }
}

/**
 * Check extensions.gnome.org for updates
 *
 * @returns {void}
 */
export async function checkForUpdates() {
    if (!Main.extensionManager.updatesSupported)
        return;

    let metadatas = {};
    Main.extensionManager.getUuids().forEach(uuid => {
        let extension = Main.extensionManager.lookup(uuid);
        if (extension.type !== ExtensionUtils.ExtensionType.PER_USER)
            return;
        if (extension.hasUpdate)
            return;
        // don't updates out of repository mode extension
        if (Desktop.is('ubuntu') && Main.extensionManager.isModeExtension(uuid))
            return;
        metadatas[uuid] = {
            version: extension.metadata.version,
        };
    });

    if (Object.keys(metadatas).length === 0)
        return; // nothing to update

    const versionCheck = global.settings.get_boolean(
        'disable-extension-version-validation');
    const params = {
        shell_version: Config.PACKAGE_VERSION,
        disable_version_validation: `${versionCheck}`,
    };
    const requestBody = new GLib.Bytes(JSON.stringify(metadatas));

    const message = Soup.Message.new('POST',
        `${REPOSITORY_URL_UPDATE}?${Soup.form_encode_hash(params)}`);
    message.set_request_body_from_bytes('application/json', requestBody);

    let json;
    try {
        const bytes = await _httpSession.send_and_read_async(
            message,
            GLib.PRIORITY_DEFAULT,
            null);
        checkResponse(message);
        json = new TextDecoder().decode(bytes.get_data());
    } catch (e) {
        log(`Update check failed: ${e.message}`);
        return;
    }

    const operations = JSON.parse(json);
    const updates = [];
    for (const uuid in operations) {
        const operation = operations[uuid];
        if (operation === 'upgrade' || operation === 'downgrade')
            updates.push(uuid);
    }

    try {
        await Promise.allSettled(
            updates.map(uuid => downloadExtensionUpdate(uuid)));
    } catch (e) {
        log(`Some extension updates failed to download: ${e.message}`);
    }
}

class ExtractError extends Error {
    get name() {
        return 'ExtractError';
    }
}

class EnableError extends Error {
    get name() {
        return 'EnableError';
    }
}

const InstallExtensionDialog = GObject.registerClass(
class InstallExtensionDialog extends ModalDialog.ModalDialog {
    _init(uuid, info, invocation) {
        super._init({styleClass: 'extension-dialog'});

        this._uuid = uuid;
        this._info = info;
        this._invocation = invocation;

        this.setButtons([{
            label: _('Cancel'),
            action: this._onCancelButtonPressed.bind(this),
            key: Clutter.KEY_Escape,
        }, {
            label: _('Install'),
            action: this._onInstallButtonPressed.bind(this),
            default: true,
        }]);

        let content = new Dialog.MessageDialogContent({
            title: _('Install Extension'),
            description: _('Download and install “%s” from extensions.gnome.org?').format(info.name),
        });

        this.contentLayout.add_child(content);
    }

    _onCancelButtonPressed() {
        this.close();
        this._invocation.return_value(GLib.Variant.new('(s)', ['cancelled']));
    }

    async _onInstallButtonPressed() {
        this.close();

        const params = {shell_version: Config.PACKAGE_VERSION};
        const message = Soup.Message.new_from_encoded_form('GET',
            REPOSITORY_URL_DOWNLOAD.format(this._uuid),
            Soup.form_encode_hash(params));

        const dir = Gio.File.new_for_path(
            GLib.build_filenamev([global.userdatadir, 'extensions', this._uuid]));

        try {
            const bytes = await _httpSession.send_and_read_async(
                message,
                GLib.PRIORITY_DEFAULT,
                null);
            checkResponse(message);

            try {
                await extractExtensionArchive(bytes, dir);
            } catch (e) {
                throw new ExtractError(e.message);
            }

            const extension = Main.extensionManager.createExtensionObject(
                this._uuid, dir, ExtensionUtils.ExtensionType.PER_USER);
            Main.extensionManager.loadExtension(extension);
            if (!Main.extensionManager.enableExtension(this._uuid))
                throw new EnableError(`Cannot enable ${this._uuid}`);

            this._invocation.return_value(new GLib.Variant('(s)', ['successful']));
        } catch (e) {
            let code;
            if (e instanceof ExtractError)
                code = ExtensionError.EXTRACT_FAILED;
            else if (e instanceof EnableError)
                code = ExtensionError.ENABLE_FAILED;
            else
                code = ExtensionError.DOWNLOAD_FAILED;

            log(`Error while installing ${this._uuid}: ${e.message}`);
            this._invocation.return_error_literal(
                ExtensionErrors, code, e.message);
        }
    }
});

export function init() {
    _httpSession = new Soup.Session();
}
(uuay)autorunManager.js// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';

import * as GnomeSession from '../../misc/gnomeSession.js';
import * as MessageTray from '../messageTray.js';

Gio._promisify(Gio.Mount.prototype, 'guess_content_type');

import {loadInterfaceXML} from '../../misc/fileUtils.js';

// GSettings keys
const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
const SETTING_DISABLE_AUTORUN = 'autorun-never';
const SETTING_START_APP = 'autorun-x-content-start-app';
const SETTING_IGNORE = 'autorun-x-content-ignore';
const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';

/** @enum {number} */
const AutorunSetting = {
    RUN: 0,
    IGNORE: 1,
    FILES: 2,
    ASK: 3,
};

// misc utils
function shouldAutorunMount(mount) {
    let root = mount.get_root();
    let volume = mount.get_volume();

    if (!volume || !volume.allowAutorun)
        return false;

    if (root.is_native() && isMountRootHidden(root))
        return false;

    return true;
}

function isMountRootHidden(root) {
    let path = root.get_path();

    // skip any mounts in hidden directory hierarchies
    return path.includes('/.');
}

function isMountNonLocal(mount) {
    // If the mount doesn't have an associated volume, that means it's
    // an uninteresting filesystem. Most devices that we care about will
    // have a mount, like media players and USB sticks.
    let volume = mount.get_volume();
    if (volume == null)
        return true;

    return volume.get_identifier('class') === 'network';
}

function startAppForMount(app, mount) {
    let files = [];
    let root = mount.get_root();
    let retval = false;

    files.push(root);

    try {
        retval = app.launch(files,
            global.create_app_launch_context(0, -1));
    } catch (e) {
        log(`Unable to launch the app ${app.get_name()}: ${e}`);
    }

    return retval;
}

const HotplugSnifferIface = loadInterfaceXML('org.gnome.Shell.HotplugSniffer');
const HotplugSnifferProxy = Gio.DBusProxy.makeProxyWrapper(HotplugSnifferIface);
function HotplugSniffer() {
    return new HotplugSnifferProxy(Gio.DBus.session,
        'org.gnome.Shell.HotplugSniffer',
        '/org/gnome/Shell/HotplugSniffer');
}

class ContentTypeDiscoverer {
    constructor() {
        this._settings = new Gio.Settings({schema_id: SETTINGS_SCHEMA});
    }

    async guessContentTypes(mount) {
        let autorunEnabled = !this._settings.get_boolean(SETTING_DISABLE_AUTORUN);
        let shouldScan = autorunEnabled && !isMountNonLocal(mount);

        let contentTypes = [];
        if (shouldScan) {
            try {
                contentTypes = await mount.guess_content_type(false, null);
            } catch (e) {
                log(`Unable to guess content types on added mount ${mount.get_name()}: ${e}`);
            }

            if (contentTypes.length === 0) {
                const root = mount.get_root();
                const hotplugSniffer = new HotplugSniffer();
                [contentTypes] = await hotplugSniffer.SniffURIAsync(root.get_uri());
            }
        }

        // we're not interested in win32 software content types here
        contentTypes = contentTypes.filter(
            type => type !== 'x-content/win32-software');

        const apps = [];
        contentTypes.forEach(type => {
            const app = Gio.app_info_get_default_for_type(type, false);

            if (app)
                apps.push(app);
        });

        if (apps.length === 0)
            apps.push(Gio.app_info_get_default_for_type('inode/directory', false));

        return [apps, contentTypes];
    }
}

class AutorunManager {
    constructor() {
        this._session = new GnomeSession.SessionManager();
        this._volumeMonitor = Gio.VolumeMonitor.get();

        this._dispatcher = new AutorunDispatcher(this);
    }

    enable() {
        this._volumeMonitor.connectObject(
            'mount-added', this._onMountAdded.bind(this),
            'mount-removed', this._onMountRemoved.bind(this), this);
    }

    disable() {
        this._volumeMonitor.disconnectObject(this);
    }

    async _onMountAdded(monitor, mount) {
        // don't do anything if our session is not the currently
        // active one
        if (!this._session.SessionIsActive)
            return;

        const discoverer = new ContentTypeDiscoverer();
        const [apps, contentTypes] = await discoverer.guessContentTypes(mount);
        this._dispatcher.addMount(mount, apps, contentTypes);
    }

    _onMountRemoved(monitor, mount) {
        this._dispatcher.removeMount(mount);
    }
}

class AutorunDispatcher {
    constructor(manager) {
        this._manager = manager;
        this._notifications = new Map();
        this._settings = new Gio.Settings({schema_id: SETTINGS_SCHEMA});
    }

    _getAutorunSettingForType(contentType) {
        let runApp = this._settings.get_strv(SETTING_START_APP);
        if (runApp.includes(contentType))
            return AutorunSetting.RUN;

        let ignore = this._settings.get_strv(SETTING_IGNORE);
        if (ignore.includes(contentType))
            return AutorunSetting.IGNORE;

        let openFiles = this._settings.get_strv(SETTING_OPEN_FOLDER);
        if (openFiles.includes(contentType))
            return AutorunSetting.FILES;

        return AutorunSetting.ASK;
    }

    _addNotification(mount, apps) {
        // Only show a new notification if there isn't already an existing one
        if (this._notifications.has(mount))
            return;

        const source = MessageTray.getSystemSource();
        const notification = new MessageTray.Notification({
            source,
            title: mount.get_name(),
        });
        notification.connect('activate', () => {
            const app = Gio.app_info_get_default_for_type('inode/directory', false);
            startAppForMount(app, mount);
        });
        apps.forEach(app => {
            notification.addAction(
                _('Open with %s').format(app.get_name()),
                () => startAppForMount(app, mount)
            );
        });
        notification.connect('destroy', () => this._notifications.delete(mount));
        this._notifications.set(mount, notification);
        source.addNotification(notification);
    }

    addMount(mount, apps, contentTypes) {
        // if autorun is disabled globally, return
        if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
            return;

        // if the mount doesn't want to be autorun, return
        if (!shouldAutorunMount(mount))
            return;

        let setting;
        if (contentTypes.length > 0)
            setting = this._getAutorunSettingForType(contentTypes[0]);
        else
            setting = AutorunSetting.ASK;

        // check at the settings for the first content type
        // to see whether we should ask
        if (setting === AutorunSetting.IGNORE)
            return; // return right away

        let success = false;
        let app = null;

        if (setting === AutorunSetting.RUN)
            app = Gio.app_info_get_default_for_type(contentTypes[0], false);
        else if (setting === AutorunSetting.FILES)
            app = Gio.app_info_get_default_for_type('inode/directory', false);

        if (app)
            success = startAppForMount(app, mount);

        // we fallback here also in case the settings did not specify 'ask',
        // but we failed launching the default app or the default file manager
        if (!success)
            this._addNotification(mount, apps);
    }

    removeMount(mount) {
        this._notifications.get(mount)?.destroy();
    }
}

export {AutorunManager as Component};
(uuay)checkBox.js�import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import St from 'gi://St';

export const CheckBox = GObject.registerClass(
class CheckBox extends St.Button {
    _init(label) {
        let container = new St.BoxLayout({
            x_expand: true,
            y_expand: true,
        });
        super._init({
            style_class: 'check-box',
            child: container,
            button_mask: St.ButtonMask.ONE,
            toggle_mode: true,
            can_focus: true,
        });
        this.set_accessible_role(Atk.Role.CHECK_BOX);

        this._box = new St.Bin({y_align: Clutter.ActorAlign.START});
        container.add_child(this._box);

        this._label = new St.Label({y_align: Clutter.ActorAlign.CENTER});
        this._label.clutter_text.set_line_wrap(true);
        this._label.clutter_text.set_ellipsize(Pango.EllipsizeMode.NONE);
        this.set_label_actor(this._label);
        container.add_child(this._label);

        if (label)
            this.setLabel(label);
    }

    setLabel(label) {
        this._label.set_text(label);
    }

    getLabelActor() {
        return this._label;
    }
});
(uuay)switchMonitor.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import St from 'gi://St';

import * as SwitcherPopup from './switcherPopup.js';

const APP_ICON_SIZE = 96;

export const SwitchMonitorPopup = GObject.registerClass(
class SwitchMonitorPopup extends SwitcherPopup.SwitcherPopup {
    _init() {
        let items = [];

        items.push({
            icon: 'shell-display-mirror-symbolic',
            /* Translators: this is for display mirroring i.e. cloning.
             * Try to keep it under around 15 characters.
             */
            label: _('Mirror'),
            configType: Meta.MonitorSwitchConfigType.ALL_MIRROR,
        });

        items.push({
            icon: 'shell-display-extend-all-symbolic',
            /* Translators: this is for the desktop spanning displays.
             * Try to keep it under around 15 characters.
             */
            label: _('Join Displays'),
            configType: Meta.MonitorSwitchConfigType.ALL_LINEAR,
        });

        if (global.backend.get_monitor_manager().has_builtin_panel) {
            items.push({
                icon: 'shell-display-external-only-symbolic',
                /* Translators: this is for using only external displays.
                 * Try to keep it under around 15 characters.
                 */
                label: _('External Only'),
                configType: Meta.MonitorSwitchConfigType.EXTERNAL,
            });
            items.push({
                icon: 'shell-display-built-in-only-symbolic',
                /* Translators: this is for using only the laptop display.
                 * Try to keep it under around 15 characters.
                 */
                label: _('Built-in Only'),
                configType: Meta.MonitorSwitchConfigType.BUILTIN,
            });
        }

        super._init(items);

        this._switcherList = new SwitchMonitorSwitcher(items);
    }

    show(backward, binding, mask) {
        if (!global.backend.get_monitor_manager().can_switch_config())
            return false;

        return super.show(backward, binding, mask);
    }

    _initialSelection() {
        let currentConfig = global.backend.get_monitor_manager().get_switch_config();
        let selectConfig = (currentConfig + 1) % this._items.length;
        this._select(selectConfig);
    }

    _keyPressHandler(keysym, action) {
        if (action === Meta.KeyBindingAction.SWITCH_MONITOR)
            this._select(this._next());
        else if (keysym === Clutter.KEY_Left)
            this._select(this._previous());
        else if (keysym === Clutter.KEY_Right)
            this._select(this._next());
        else
            return Clutter.EVENT_PROPAGATE;

        return Clutter.EVENT_STOP;
    }

    _finish() {
        super._finish();

        const monitorManager = global.backend.get_monitor_manager();
        const item = this._items[this._selectedIndex];

        monitorManager.switch_config(item.configType);
    }
});

const SwitchMonitorSwitcher = GObject.registerClass(
class SwitchMonitorSwitcher extends SwitcherPopup.SwitcherList {
    _init(items) {
        super._init(true);

        for (let i = 0; i < items.length; i++)
            this._addIcon(items[i]);
    }

    _addIcon(item) {
        const box = new St.BoxLayout({
            style_class: 'alt-tab-app',
            vertical: true,
        });

        const icon = new St.Icon({
            icon_name: item.icon,
            icon_size: APP_ICON_SIZE,
        });
        box.add_child(icon);

        let text = new St.Label({
            text: item.label,
            x_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(text);

        this.addItem(box, text);
    }
});
(uuay)bluetooth.js�9// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
import 'gi://GnomeBluetooth?version=3.0';

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GnomeBluetooth from 'gi://GnomeBluetooth';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import St from 'gi://St';

import {Spinner} from '../animation.js';
import * as PopupMenu from '../popupMenu.js';
import {QuickMenuToggle, SystemIndicator} from '../quickSettings.js';

import {loadInterfaceXML} from '../../misc/fileUtils.js';

const {AdapterState} = GnomeBluetooth;

const BUS_NAME = 'org.gnome.SettingsDaemon.Rfkill';
const OBJECT_PATH = '/org/gnome/SettingsDaemon/Rfkill';

const RfkillManagerInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Rfkill');
const rfkillManagerInfo = Gio.DBusInterfaceInfo.new_for_xml(RfkillManagerInterface);

Gio._promisify(GnomeBluetooth.Client.prototype, 'connect_service');

const STATE_CHANGE_FAILED_TIMEOUT_MS = 30 * 1000;

const BtClient = GObject.registerClass({
    Properties: {
        'available': GObject.ParamSpec.boolean('available', '', '',
            GObject.ParamFlags.READABLE,
            false),
        'active': GObject.ParamSpec.boolean('active', '', '',
            GObject.ParamFlags.READABLE,
            false),
        'adapter-state': GObject.ParamSpec.enum('adapter-state', '', '',
            GObject.ParamFlags.READABLE,
            AdapterState, AdapterState.ABSENT),
    },
    Signals: {
        'devices-changed': {},
        'device-removed': {param_types: [GObject.TYPE_STRING]},
    },
}, class BtClient extends GObject.Object {
    _init() {
        super._init();

        this._client = new GnomeBluetooth.Client();
        this._client.connect('notify::default-adapter-powered', () => {
            this.notify('active');
        });
        this._client.connect('notify::default-adapter-state', () => {
            delete this._predictedState;
            this.notify('adapter-state');
        });
        this._client.connect('notify::default-adapter', () => {
            const newAdapter = this._client.default_adapter ?? null;

            this._adapter = newAdapter;
            this._deviceNotifyConnected.clear();
            this.emit('devices-changed');

            this.notify('active');
        });

        this._proxy = new Gio.DBusProxy({
            g_connection: Gio.DBus.session,
            g_name: BUS_NAME,
            g_object_path: OBJECT_PATH,
            g_interface_name: rfkillManagerInfo.name,
            g_interface_info: rfkillManagerInfo,
        });
        this._proxy.connect('g-properties-changed', (p, properties) => {
            const changedProperties = properties.unpack();
            if ('BluetoothHardwareAirplaneMode' in changedProperties)
                this.notify('available');
            else if ('BluetoothHasAirplaneMode' in changedProperties)
                this.notify('available');
        });
        this._proxy.init_async(GLib.PRIORITY_DEFAULT, null)
            .catch(e => console.error(e.message));

        this._adapter = null;

        this._deviceNotifyConnected = new Set();

        const deviceStore = this._client.get_devices();
        for (let i = 0; i < deviceStore.get_n_items(); i++)
            this._connectDeviceNotify(deviceStore.get_item(i));

        this._client.connect('device-removed', (c, path) => {
            this._deviceNotifyConnected.delete(path);
            this.emit('device-removed', path);
            this.emit('devices-changed');
        });
        this._client.connect('device-added', (c, device) => {
            this._connectDeviceNotify(device);
            this.emit('devices-changed');
        });
    }

    get available() {
        // If we have an rfkill switch, make sure it's not a hardware
        // one as we can't get out of it in software
        return this._proxy.BluetoothHasAirplaneMode &&
            !this._proxy.BluetoothHardwareAirplaneMode;
    }

    get active() {
        return this._client.default_adapter_powered;
    }

    get adapter_state() {
        if (this._predictedState !== undefined)
            return this._predictedState;
        return this._client.default_adapter_state;
    }

    toggleActive() {
        const {active} = this;

        // on many systems, there's a significant delay until the rfkill
        // state results in an adapter state change; work around that by
        // overriding the current state with the expected transition
        this._predictedState = active
            ? AdapterState.TURNING_OFF
            : AdapterState.TURNING_ON;
        this.notify('adapter-state');

        // toggling the state *really* should result in an adapter-state
        // change eventually (even on error), but just to be sure to not
        // be stuck with the overriden state, force a notify signal after
        // a timeout
        setTimeout(() => this._client.notify('default-adapter-state'),
            STATE_CHANGE_FAILED_TIMEOUT_MS);

        this._proxy.BluetoothAirplaneMode = active;
        if (!this._client.default_adapter_powered)
            this._client.default_adapter_powered = true;
    }

    async toggleDevice(device) {
        const connect = !device.connected;
        console.debug(`${connect
            ? 'Connect' : 'Disconnect'} device "${device.name}"`);

        try {
            await this._client.connect_service(
                device.get_object_path(),
                connect,
                null);
            console.debug(`Device "${device.name}" ${
                connect ? 'connected' : 'disconnected'}`);
        } catch (e) {
            console.error(`Failed to ${connect
                ? 'connect' : 'disconnect'} device "${device.name}": ${e.message}`);
        }
    }

    *getDevices() {
        // Ignore any lingering device references when turned off
        if (!this.active)
            return;

        const deviceStore = this._client.get_devices();

        for (let i = 0; i < deviceStore.get_n_items(); i++) {
            const device = deviceStore.get_item(i);

            if (device.paired || device.trusted)
                yield device;
        }
    }

    _queueDevicesChanged() {
        if (this._devicesChangedId)
            return;
        this._devicesChangedId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
            delete this._devicesChangedId;
            this.emit('devices-changed');
            return GLib.SOURCE_REMOVE;
        });
    }

    _connectDeviceNotify(device) {
        const path = device.get_object_path();

        if (this._deviceNotifyConnected.has(path))
            return;

        device.connect('notify::alias', () => this._queueDevicesChanged());
        device.connect('notify::paired', () => this._queueDevicesChanged());
        device.connect('notify::trusted', () => this._queueDevicesChanged());
        device.connect('notify::connected', () => this._queueDevicesChanged());

        this._deviceNotifyConnected.add(path);
    }
});

const BluetoothDeviceItem = GObject.registerClass(
class BluetoothDeviceItem extends PopupMenu.PopupBaseMenuItem {
    constructor(device, client) {
        super({
            style_class: 'bt-device-item',
        });

        this._device = device;
        this._client = client;

        this._icon = new St.Icon({
            style_class: 'popup-menu-icon',
        });
        this.add_child(this._icon);

        this._label = new St.Label({
            x_expand: true,
        });
        this.add_child(this._label);

        this._subtitle = new St.Label({
            style_class: 'device-subtitle',
        });
        this.add_child(this._subtitle);

        this._spinner = new Spinner(16, {hideOnStop: true});
        this.add_child(this._spinner);

        this._spinner.bind_property('visible',
            this._subtitle, 'visible',
            GObject.BindingFlags.SYNC_CREATE |
            GObject.BindingFlags.INVERT_BOOLEAN);

        this._device.bind_property('connectable',
            this, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        this._device.bind_property('icon',
            this._icon, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE);
        this._device.bind_property('alias',
            this._label, 'text',
            GObject.BindingFlags.SYNC_CREATE);
        this._device.bind_property_full('connected',
            this._subtitle, 'text',
            GObject.BindingFlags.SYNC_CREATE,
            (bind, source) => [true, source ? _('Disconnect') : _('Connect')],
            null);

        this.connect('destroy', () => (this._spinner = null));
        this.connect('activate', () => this._toggleConnected().catch(logError));
    }

    async _toggleConnected() {
        this._spinner.play();
        await this._client.toggleDevice(this._device);
        this._spinner?.stop();
    }
});

const BluetoothToggle = GObject.registerClass(
class BluetoothToggle extends QuickMenuToggle {
    _init(client) {
        super._init({title: _('Bluetooth')});

        this.menu.setHeader('bluetooth-active-symbolic', _('Bluetooth'));

        this._deviceItems = new Map();
        this._deviceSection = new PopupMenu.PopupMenuSection();
        this.menu.addMenuItem(this._deviceSection);

        this._placeholderItem = new PopupMenu.PopupMenuItem('', {
            style_class: 'bt-menu-placeholder',
            reactive: false,
            can_focus: false,
        });
        this._placeholderItem.label.clutter_text.set({
            ellipsize: Pango.EllipsizeMode.NONE,
            line_wrap: true,
        });
        this.menu.addMenuItem(this._placeholderItem);

        this._deviceSection.actor.bind_property('visible',
            this._placeholderItem, 'visible',
            GObject.BindingFlags.SYNC_CREATE |
            GObject.BindingFlags.INVERT_BOOLEAN);

        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
        this.menu.addSettingsAction(_('Bluetooth Settings'),
            'gnome-bluetooth-panel.desktop');

        this._client = client;

        this._client.bind_property('available',
            this, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        this._client.bind_property('active',
            this, 'checked',
            GObject.BindingFlags.SYNC_CREATE);
        this._client.bind_property_full('adapter-state',
            this, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE,
            (bind, source) => [true, this._getIconNameFromState(source)],
            null);

        this._client.connectObject(
            'notify::active', () => this._onActiveChanged(),
            'devices-changed', () => this._sync(),
            'device-removed', (c, path) => this._removeDevice(path),
            this);

        this.menu.connect('open-state-changed', isOpen => {
            // We don't reorder the list while the menu is open,
            // so do it now to start with the proper order
            if (isOpen)
                this._reorderDeviceItems();
        });

        this.connect('clicked', () => this._client.toggleActive());

        this._updatePlaceholder();
        this._sync();
    }

    _onActiveChanged() {
        this._updatePlaceholder();

        this._deviceItems.forEach(item => item.destroy());
        this._deviceItems.clear();

        this._sync();
    }

    _updatePlaceholder() {
        this._placeholderItem.label.text = this._client.active
            ? _('No available or connected devices')
            : _('Turn on Bluetooth to connect to devices');
    }

    _updateDeviceVisibility() {
        this._deviceSection.actor.visible =
            [...this._deviceItems.values()].some(item => item.visible);
    }

    _getSortedDevices() {
        return [...this._client.getDevices()].sort((dev1, dev2) => {
            if (dev1.connected !== dev2.connected)
                return dev2.connected - dev1.connected;
            return dev1.alias.localeCompare(dev2.alias);
        });
    }

    _removeDevice(path) {
        this._deviceItems.get(path)?.destroy();
        this._deviceItems.delete(path);

        this._updateDeviceVisibility();
    }

    _reorderDeviceItems() {
        const devices = this._getSortedDevices();
        for (const [i, dev] of devices.entries()) {
            const item = this._deviceItems.get(dev.get_object_path());
            if (!item)
                continue;

            this._deviceSection.moveMenuItem(item, i);
        }
    }

    _sync() {
        const devices = this._getSortedDevices();

        for (const dev of devices) {
            const path = dev.get_object_path();
            if (this._deviceItems.has(path))
                continue;

            const item = new BluetoothDeviceItem(dev, this._client);
            item.connect('notify::visible', () => this._updateDeviceVisibility());

            this._deviceSection.addMenuItem(item);
            this._deviceItems.set(path, item);
        }

        const connectedDevices = devices.filter(dev => dev.connected);
        const nConnected = connectedDevices.length;

        if (nConnected > 1)
            /* Translators: This is the number of connected bluetooth devices */
            this.subtitle = ngettext('%d Connected', '%d Connected', nConnected).format(nConnected);
        else if (nConnected === 1)
            this.subtitle = connectedDevices[0].alias;
        else
            this.subtitle = null;

        this._updateDeviceVisibility();
    }

    _getIconNameFromState(state) {
        switch (state) {
        case AdapterState.ON:
            return 'bluetooth-active-symbolic';
        case AdapterState.OFF:
        case AdapterState.ABSENT:
            return 'bluetooth-disabled-symbolic';
        case AdapterState.TURNING_ON:
        case AdapterState.TURNING_OFF:
            return 'bluetooth-acquiring-symbolic';
        default:
            console.warn(`Unexpected state ${
                GObject.enum_to_string(AdapterState, state)}`);
            return '';
        }
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this._client = new BtClient();
        this._client.connect('devices-changed', () => this._sync());

        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'bluetooth-active-symbolic';

        this.quickSettingsItems.push(new BluetoothToggle(this._client));

        this._sync();
    }

    _sync() {
        const devices = [...this._client.getDevices()];
        const connectedDevices = devices.filter(dev => dev.connected);
        const nConnectedDevices = connectedDevices.length;

        this._indicator.visible = nConnectedDevices > 0;
    }
});
(uuay)authPrompt.jsay// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Animation from '../ui/animation.js';
import * as AuthList from './authList.js';
import * as AuthNotification from '../gdm/authNotification.js';
import * as Batch from './batch.js';
import * as GdmUtil from './util.js';
import * as Main from '../ui/main.js';
import * as MessageTray from '../ui/messageTray.js';
import * as Params from '../misc/params.js';
import * as ShellEntry from '../ui/shellEntry.js';
import * as UserWidget from '../ui/userWidget.js';
import * as WebLogin from './webLogin.js';
import {wiggle} from '../misc/animationUtils.js';

const DEFAULT_BUTTON_WELL_ICON_SIZE = 16;
const DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000;
const DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300;

const MESSAGE_FADE_OUT_ANIMATION_TIME = 500;

/** @enum {number} */
export const AuthPromptMode = {
    UNLOCK_ONLY: 0,
    UNLOCK_OR_LOG_IN: 1,
};

/** @enum {number} */
export const AuthPromptStatus = {
    NOT_VERIFYING: 0,
    VERIFYING: 1,
    VERIFICATION_FAILED: 2,
    VERIFICATION_SUCCEEDED: 3,
    VERIFICATION_CANCELLED: 4,
    VERIFICATION_IN_PROGRESS: 5,
};

/** @enum {number} */
export const BeginRequestType = {
    PROVIDE_USERNAME: 0,
    DONT_PROVIDE_USERNAME: 1,
    REUSE_USERNAME: 2,
};

export const AuthPrompt = GObject.registerClass({
    Signals: {
        'cancelled': {},
        'failed': {},
        'next': {},
        'prompted': {},
        'mechanisms-changed': {param_types: [GObject.TYPE_STRING]},
        'reset': {param_types: [GObject.TYPE_UINT]},
    },
    Properties: {
        'verification-status': GObject.ParamSpec.uint(
            'verification-status', 'verification-status', 'verification-status',
            GObject.ParamFlags.READWRITE,
            AuthPromptStatus.NOT_VERIFYING, AuthPromptStatus.VERIFICATION_IN_PROGRESS, 0),
    },
}, class AuthPrompt extends St.BoxLayout {
    _init(gdmClient, mode) {
        super._init({
            style_class: 'login-dialog-prompt-layout',
            vertical: true,
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            reactive: true,
        });

        this.verificationStatus = AuthPromptStatus.NOT_VERIFYING;

        this._gdmClient = gdmClient;
        this._mode = mode;
        this._defaultButtonWellActor = null;
        this._cancelledRetries = 0;

        let reauthenticationOnly;
        if (this._mode === AuthPromptMode.UNLOCK_ONLY)
            reauthenticationOnly = true;
        else if (this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN)
            reauthenticationOnly = false;

        this._userVerifier = new GdmUtil.ShellUserVerifier(this._gdmClient, {reauthenticationOnly});

        this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this));
        this._userVerifier.connect('show-message', this._onShowMessage.bind(this));
        this._userVerifier.connect('show-waiting-message', this._onWaitingMessage.bind(this));
        this._userVerifier.connect('show-choice-list', this._onShowChoiceList.bind(this));
        this._userVerifier.connect('mechanisms-list-changed', this._onAuthMechanismsListChanged.bind(this));
        this._userVerifier.connect('foreground-mechanism-changed', this._onForegroundMechanismChanged.bind(this));
        this._userVerifier.connect('choice-list-selected', (_, ...args) => this._onChoiceListSelected(...args));
        this._userVerifier.connect('web-login', this._onWebLogin.bind(this));
        this._userVerifier.connect('web-login-time-out', this._onWebLoginTimeOut.bind(this));
        this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this));
        this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this));
        this._userVerifier.connect('reset', this._onReset.bind(this));
        this._userVerifier.connect('smartcard-status-changed', this._onSmartcardStatusChanged.bind(this));
        this._userVerifier.connect('credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this));
        this.smartcardDetected = this._userVerifier.smartcardDetected;

        this.connect('destroy', this._onDestroy.bind(this));
        this.mechanisms = new Map();

        this._userWell = new St.Bin({
            x_expand: true,
            y_expand: true,
        });
        this.add_child(this._userWell);

        this._hasCancelButton = this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN;

        this._initInputRow();

        let capsLockPlaceholder = new St.Label();
        this.add_child(capsLockPlaceholder);

        this._capsLockWarningLabel = new ShellEntry.CapsLockWarning({
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this._capsLockWarningLabel);

        this._capsLockWarningLabel.bind_property('visible',
            capsLockPlaceholder, 'visible',
            GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN);

        this._message = new St.Label({
            opacity: 0,
            styleClass: 'login-dialog-message',
            y_expand: true,
            x_expand: true,
            y_align: Clutter.ActorAlign.START,
            x_align: Clutter.ActorAlign.CENTER,
        });
        this._message.clutter_text.line_wrap = true;
        this._message.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this.add_child(this._message);
    }

    _onDestroy() {
        this._inactiveEntry.destroy();
        this._inactiveEntry = null;
        this._userVerifier.destroy();
        this._userVerifier = null;
    }

    vfunc_key_press_event(event) {
        if (event.get_key_symbol() === Clutter.KEY_Escape)
            this._handleCancel();
        return super.vfunc_key_press_event(event);
    }

    _handleCancel() {
        if (this._userVerifier.cancelRequested())
            return;

        this.cancel();
    }

    _initInputRow() {
        this._mainBox = new St.BoxLayout({
            style_class: 'login-dialog-button-box',
            vertical: false,
        });
        this.add_child(this._mainBox);

        this.cancelButton = new St.Button({
            style_class: 'login-dialog-button cancel-button',
            accessible_name: _('Cancel'),
            button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
            reactive: this._hasCancelButton,
            can_focus: this._hasCancelButton,
            x_align: Clutter.ActorAlign.END,
            y_align: Clutter.ActorAlign.CENTER,
            icon_name: 'go-previous-symbolic',
        });
        if (this._hasCancelButton)
            this.cancelButton.connect('clicked', () => this._handleCancel());
        else
            this.cancelButton.opacity = 0;
        this._mainBox.add_child(this.cancelButton);

        this._webLoginPromptWell = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            x_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._mainBox.add_child(this._webLoginPromptWell);

        this._authList = new AuthList.AuthList();
        this._authList.set({
            visible: false,
        });
        this._authList.connect('activate', (list, key) => {
            this._authList.reactive = false;
            this._authList.ease({
                opacity: 0,
                duration: MESSAGE_FADE_OUT_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this._authList.clear();
                    this._authList.hide();
                    this._userVerifier.selectChoice(this._queryingService, key);
                },
            });
        });
        this._mainBox.add_child(this._authList);

        let entryParams = {
            style_class: 'login-dialog-prompt-entry',
            can_focus: true,
            x_expand: true,
        };

        this._entry = null;

        this._textEntry = new St.Entry(entryParams);
        ShellEntry.addContextMenu(this._textEntry, {actionMode: Shell.ActionMode.NONE});

        this._passwordEntry = new St.PasswordEntry(entryParams);
        ShellEntry.addContextMenu(this._passwordEntry, {actionMode: Shell.ActionMode.NONE});

        this._entry = this._passwordEntry;
        this._mainBox.add_child(this._entry);
        this._entry.grab_key_focus();
        this._inactiveEntry = this._textEntry;

        this._timedLoginIndicator = new St.Bin({
            style_class: 'login-dialog-timed-login-indicator',
            scale_x: 0,
        });

        this.add_child(this._timedLoginIndicator);

        [this._textEntry, this._passwordEntry].forEach(entry => {
            entry.clutter_text.connect('text-changed', () => {
                if (!this._userVerifier.hasPendingMessages)
                    this._fadeOutMessage();
            });

            entry.clutter_text.connect('activate', () => {
                let shouldSpin = entry === this._passwordEntry;
                if (entry.reactive)
                    this._activateNext(shouldSpin);
            });
        });

        this._defaultButtonWell = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            x_align: Clutter.ActorAlign.END,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._defaultButtonWell.add_constraint(new Clutter.BindConstraint({
            source: this.cancelButton,
            coordinate: Clutter.BindCoordinate.WIDTH,
        }));
        this._mainBox.add_child(this._defaultButtonWell);

        this._spinner = new Animation.Spinner(DEFAULT_BUTTON_WELL_ICON_SIZE);
        this._defaultButtonWell.add_child(this._spinner);
    }

    showTimedLoginIndicator(time) {
        let hold = new Batch.Hold();

        this.hideTimedLoginIndicator();

        const startTime = GLib.get_monotonic_time();

        this._timedLoginTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 33,
            () => {
                const currentTime = GLib.get_monotonic_time();
                const elapsedTime = (currentTime - startTime) / GLib.USEC_PER_SEC;
                this._timedLoginIndicator.scale_x = elapsedTime / time;
                if (elapsedTime >= time) {
                    this._timedLoginTimeoutId = 0;
                    hold.release();
                    return GLib.SOURCE_REMOVE;
                }

                return GLib.SOURCE_CONTINUE;
            });

        GLib.Source.set_name_by_id(this._timedLoginTimeoutId, '[gnome-shell] this._timedLoginTimeoutId');

        return hold;
    }

    hideTimedLoginIndicator() {
        if (this._timedLoginTimeoutId) {
            GLib.source_remove(this._timedLoginTimeoutId);
            this._timedLoginTimeoutId = 0;
        }
        this._timedLoginIndicator.scale_x = 0.;
    }

    _activateNext(shouldSpin) {
        this.verificationStatus = AuthPromptStatus.VERIFICATION_IN_PROGRESS;
        this.updateSensitivity(false);

        if (shouldSpin)
            this.startSpinning();

        if (this._queryingService)
            this._userVerifier.answerQuery(this._queryingService, this._entry.text);
        else
            this._preemptiveAnswer = this._entry.text;

        this.emit('next');
    }

    _updateEntry(secret) {
        if (secret && this._entry !== this._passwordEntry) {
            this._mainBox.replace_child(this._entry, this._passwordEntry);
            this._entry = this._passwordEntry;
            this._inactiveEntry = this._textEntry;
        } else if (!secret && this._entry !== this._textEntry) {
            this._mainBox.replace_child(this._entry, this._textEntry);
            this._entry = this._textEntry;
            this._inactiveEntry = this._passwordEntry;
        }
        this._capsLockWarningLabel.visible = secret;

        this._webLoginPromptWell.visible = false;
        this._entry.visible = true;
    }

    _onAskQuestion(verifier, serviceName, question, secret) {
        if (this._queryingService)
            this.clear();

        this._queryingService = serviceName;
        if (this._preemptiveAnswer) {
            this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer);
            this._preemptiveAnswer = null;
            return;
        }

        this._updateEntry(secret);

        // Hack: The question string comes directly from PAM, if it's "Password:"
        // we replace it with our own to allow localization, if it's something
        // else we remove the last colon and any trailing or leading spaces.
        if (question === 'Password:' || question === 'Password: ')
            this.setQuestion(_('Password'));
        else
            this.setQuestion(question.replace(/[::] *$/, '').trim());

        this.updateSensitivity(true);
        this.emit('prompted');
    }

    _onWaitingMessage(verifier, serviceName, info) {
        if (this._queryingService)
            this.clear();

        this._queryingService = serviceName;
        this.setMessage(info, GdmUtil.MessageType.INFO);
        this.startSpinning();
        this.emit('prompted');

        // FIXME: we should stop spinning and unset the querying service when done.
    }

    _onShowChoiceList(userVerifier, serviceName, promptMessage, choiceList) {
        if (this._queryingService)
            this.clear();

        this._queryingService = serviceName;

        if (this._preemptiveAnswer)
            this._preemptiveAnswer = null;

        this.setChoiceList(promptMessage, choiceList);
        this.updateSensitivity(true);
        this.emit('prompted');
    }

    _onAuthMechanismsListChanged(userVerifier, serviceName, mechanismsList) {
        this.mechanisms.set(serviceName, mechanismsList);
        this.emit('mechanisms-changed', serviceName);
    }

    _onChoiceListSelected(service, key) {
        if (this._authList?.visible)
            this._authList.emit('activate', key);
    }

    _onWebLogin(userVerifier, serviceName, introMessage, linkMessage, uri, code) {
        const introAlreadyUp = this._queryingService === serviceName;

        if (this._queryingService)
            this.clear();

        this._entry.visible = false;

        if (this._preemptiveAnswer)
            this._preemptiveAnswer = null;

        this._webLoginPromptWell.remove_all_children();

        if (this._spinner)
            this._spinner.stop();

        if (!introAlreadyUp) {
            this._webLoginIntro = new WebLogin.WebLoginIntro({message: introMessage});
            this._webLoginIntro.set({
                x_expand: true,
                y_align: Clutter.ActorAlign.START,
            });
            this._webLoginIntro.connect('clicked', () => {
                this._queryingService = serviceName;

                if (this._webLoginTimedOut)
                    this._refreshWebLogin(serviceName);
                else
                    this._openWebLoginDialog(userVerifier, serviceName, linkMessage, uri, code);
            });
            this._webLoginPromptWell.add_child(this._webLoginIntro);
            this._webLoginPromptWell.visible = true;

            this.updateSensitivity(true);
        } else {
            this._openWebLoginDialog(userVerifier, serviceName, linkMessage, uri, code);
        }

        this._webLoginTimedOut = false;
        this.emit('prompted');
    }

    _refreshWebLogin(serviceName) {
        this.reset({
            beginRequestType: BeginRequestType.REUSE_USERNAME,
            queryingService: serviceName,
        });
    }

    _onWebLoginTimeOut(userVerifier, serviceName) {
        this._webLoginTimedOut = true;

        if (this._queryingService !== serviceName)
            return;

        this._refreshWebLogin(serviceName);
    }

    _closeWebLoginDialog() {
        if (!this._webLoginDialog)
            return;

        this._webLoginDialog.close();
        this._webLoginDialog = null;
    }

    _openWebLoginDialog(userVerifier, serviceName, message, url, code) {
        if (this._queryingService)
            this.clear();

        this._closeWebLoginDialog();

        this._queryingService = serviceName;

        this._webLoginPromptWell.remove_all_children();

        this._webLoginDialog = new WebLogin.WebLoginDialog({message, url, code});
        this._webLoginDialog.open(global.get_current_time());
        this._webLoginDialog.connect('cancel', () => {
            if (userVerifier.cancelRequested()) {
                this._closeWebLoginDialog();
                return;
            }
            if (this.verificationStatus !== AuthPromptStatus.VERIFICATION_SUCCEEDED)
                this.reset();
        });
        this._webLoginDialog.connect('done', () => {
            userVerifier.connectObject(
                `service-request::${serviceName}`, this._closeWebLoginDialog.bind(this),

                'verification-complete', this._closeWebLoginDialog.bind(this),

                'verification-failed', () => {
                    this._closeWebLoginDialog();
                    this.showLoginFailedNotification();
                    this.reset();
                },
                this);
            userVerifier.webLoginDone(serviceName);
        });

        if (this._spinner)
            this._spinner.stop();

        this.updateSensitivity(true);
        this.emit('prompted');
    }

    _onCredentialManagerAuthenticated() {
        if (this.verificationStatus !== AuthPromptStatus.VERIFICATION_SUCCEEDED)
            this.reset();
    }

    _onSmartcardStatusChanged() {
        this.smartcardDetected = this._userVerifier.smartcardDetected;

        // Most of the time we want to reset if the user inserts or removes
        // a smartcard. Smartcard insertion "preempts" what the user was
        // doing, and smartcard removal aborts the preemption.
        // The exceptions are: 1) Don't reset on smartcard insertion if we're already verifying
        //                        with a smartcard
        //                     2) Don't reset if we've already succeeded at verification and
        //                        the user is getting logged in.
        if (this._userVerifier.serviceIsDefault(GdmUtil.SMARTCARD_SERVICE_NAME) &&
            (this.verificationStatus === AuthPromptStatus.VERIFYING ||
             this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) &&
            this.smartcardDetected)
            return;

        if (this.verificationStatus !== AuthPromptStatus.VERIFICATION_SUCCEEDED)
            this.reset();
    }

    _onForegroundMechanismChanged() {
        if (this.verificationStatus !== AuthPromptStatus.VERIFICATION_SUCCEEDED)
            this.reset({beginRequestType: BeginRequestType.REUSE_USERNAME});
    }

    _onShowMessage(_userVerifier, serviceName, message, type) {
        let wiggleParameters = {duration: 0};

        if (type === GdmUtil.MessageType.ERROR &&
            this._userVerifier.serviceIsFingerprint(serviceName)) {
            // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue
            wiggleParameters = {
                duration: 65,
                wiggleCount: 3,
            };
            this._userVerifier.increaseCurrentMessageTimeout(
                wiggleParameters.duration * (wiggleParameters.wiggleCount + 2));
        }

        this.setMessage(message, type, wiggleParameters);
        this.emit('prompted');
    }

    _onVerificationFailed(userVerifier, serviceName, canRetry) {
        const wasQueryingService = this._queryingService === serviceName;

        if (wasQueryingService) {
            this._queryingService = null;
            this.clear();
        }

        this.updateSensitivity(canRetry);
        this.setActorInDefaultButtonWell(null);

        if (!canRetry)
            this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED;

        if (wasQueryingService)
            wiggle(this._entry);
    }

    _onVerificationComplete() {
        this.setActorInDefaultButtonWell(null);
        this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED;
        this.cancelButton.reactive = false;
        this.cancelButton.can_focus = false;
        this.cancelButton.ease({
            opacity: 0,
            duration: MESSAGE_FADE_OUT_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    _onReset() {
        this.verificationStatus = AuthPromptStatus.NOT_VERIFYING;
        this.reset();
    }

    setActorInDefaultButtonWell(actor, animate) {
        if (!this._defaultButtonWellActor &&
            !actor)
            return;

        let oldActor = this._defaultButtonWellActor;

        if (oldActor)
            oldActor.remove_all_transitions();

        let wasSpinner;
        if (oldActor === this._spinner)
            wasSpinner = true;
        else
            wasSpinner = false;

        let isSpinner;
        if (actor === this._spinner)
            isSpinner = true;
        else
            isSpinner = false;

        if (this._defaultButtonWellActor !== actor && oldActor) {
            if (!animate) {
                oldActor.opacity = 0;

                if (wasSpinner) {
                    if (this._spinner)
                        this._spinner.stop();
                }
            } else {
                oldActor.ease({
                    opacity: 0,
                    duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME,
                    delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY,
                    mode: Clutter.AnimationMode.LINEAR,
                    onComplete: () => {
                        if (wasSpinner) {
                            if (this._spinner)
                                this._spinner.stop();
                        }
                    },
                });
            }
        }

        if (actor) {
            if (isSpinner)
                this._spinner.play();

            if (!animate) {
                actor.opacity = 255;
            } else {
                actor.ease({
                    opacity: 255,
                    duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME,
                    delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY,
                    mode: Clutter.AnimationMode.LINEAR,
                });
            }
        }

        this._defaultButtonWellActor = actor;
    }

    startSpinning() {
        this.setActorInDefaultButtonWell(this._spinner, true);
    }

    stopSpinning() {
        this.setActorInDefaultButtonWell(null, false);
    }

    clear() {
        this._entry.text = '';
        this.stopSpinning();
        this._authList.clear();
        this._authList.hide();
        this._webLoginPromptWell.remove_all_children();
        this._webLoginPromptWell.visible = false;
    }

    setQuestion(question) {
        this._entry.hint_text = question;

        this._authList.hide();
        this._closeWebLoginDialog();

        this._entry.show();
        this._entry.grab_key_focus();
    }

    _fadeInChoiceList() {
        this._authList.set({
            opacity: 0,
            visible: true,
            reactive: false,
        });
        this._authList.ease({
            opacity: 255,
            duration: MESSAGE_FADE_OUT_ANIMATION_TIME,
            transition: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => (this._authList.reactive = true),
        });
    }

    setChoiceList(promptMessage, choiceList) {
        this._authList.clear();
        this._authList.label.text = promptMessage;
        for (let key in choiceList) {
            let text = choiceList[key];
            this._authList.addItem(key, text);
        }

        this._entry.hide();
        if (this._message.text === '')
            this._message.hide();
        this._fadeInChoiceList();
    }

    getAnswer() {
        let text;

        if (this._preemptiveAnswer) {
            text = this._preemptiveAnswer;
            this._preemptiveAnswer = null;
        } else {
            text = this._entry.get_text();
        }

        return text;
    }

    _fadeOutMessage() {
        if (this._message.opacity === 0)
            return;
        this._message.remove_all_transitions();
        this._message.ease({
            opacity: 0,
            duration: MESSAGE_FADE_OUT_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    setMessage(message, type, wiggleParameters = {duration: 0}) {
        if (type === GdmUtil.MessageType.ERROR)
            this._message.add_style_class_name('login-dialog-message-warning');
        else
            this._message.remove_style_class_name('login-dialog-message-warning');

        if (type === GdmUtil.MessageType.HINT)
            this._message.add_style_class_name('login-dialog-message-hint');
        else
            this._message.remove_style_class_name('login-dialog-message-hint');

        this._message.show();
        if (message) {
            this._message.remove_all_transitions();
            this._message.text = message;
            this._message.opacity = 255;
        } else {
            this._message.opacity = 0;
        }

        wiggle(this._message, wiggleParameters);
    }

    updateSensitivity(sensitive) {
        if (this._webLoginDialog)
            return;

        if (this._entry.reactive === sensitive)
            return;

        this._entry.reactive = sensitive;

        if (sensitive) {
            this._entry.grab_key_focus();
        } else {
            this.grab_key_focus();

            if (this._entry === this._passwordEntry)
                this._entry.password_visible = false;
        }
    }

    vfunc_hide() {
        this.setActorInDefaultButtonWell(null, true);
        super.vfunc_hide();
        this._message.opacity = 0;

        this.setUser(null);

        this.updateSensitivity(true);
        this._entry.set_text('');
    }

    setUser(user) {
        let oldChild = this._userWell.get_child();
        if (oldChild)
            oldChild.destroy();

        let userWidget = new UserWidget.UserWidget(user, Clutter.Orientation.VERTICAL);
        this._userWell.set_child(userWidget);

        if (!user)
            this._updateEntry(false);
    }

    setForegroundMechanism(mechanism) {
        this._userVerifier.setForegroundService(mechanism.serviceName);
        this._userVerifier.setForegroundMechanism(mechanism);
    }

    showLoginFailedNotification() {
        const source = new AuthNotification.AuthNotificationSource();
        Main.messageTray.add(source);

        const gicon = new Gio.ThemedIcon({name: 'dialog-password-symbolic'});
        this._loginFailedNotification = new MessageTray.Notification({
            source,
            title: _('Login Failed'),
            body: _('Please try again'),
            gicon,
            isTransient: true,
        });

        source.addNotification(this._loginFailedNotification);
    }

    hideLoginFailedNotification() {
        if (!this._loginFailedNotification)
            return;

        this._loginFailedNotification.destroy();
        this._loginFailedNotification = null;
    }

    reset(params) {
        let {beginRequestType, queryingService} = Params.parse(params, {
            beginRequestType: null,
            queryingService: null,
        });

        let oldStatus = this.verificationStatus;
        this.verificationStatus = AuthPromptStatus.NOT_VERIFYING;
        this.cancelButton.reactive = this._hasCancelButton;
        this.cancelButton.can_focus = this._hasCancelButton;
        this._preemptiveAnswer = null;
        this.cancelButton.opacity = this._hasCancelButton ? 255 : 0;

        if (this._userVerifier)
            this._userVerifier.cancel();

        this._queryingService = queryingService;
        this.clear();
        this._message.opacity = 0;
        this.setUser(null);
        this._updateEntry(true);
        this.stopSpinning();

        if (oldStatus === AuthPromptStatus.VERIFICATION_FAILED)
            this.emit('failed');
        else if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED)
            this.emit('cancelled');

        if (beginRequestType === null) {
            if (this._mode === AuthPromptMode.UNLOCK_ONLY) {
                // The user is constant at the unlock screen, so it will immediately
                // respond to the request with the username
                if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED)
                    return;
                beginRequestType = BeginRequestType.PROVIDE_USERNAME;
            } else if (this._userVerifier.foregroundServiceDeterminesUsername()) {
                // We don't need to know the username if the user preempted the login screen
                // with a smartcard or with preauthenticated oVirt credentials
                beginRequestType = BeginRequestType.DONT_PROVIDE_USERNAME;
            } else if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) {
                // We're going back to retry with current user
                beginRequestType = BeginRequestType.REUSE_USERNAME;
            } else {
                // In all other cases, we should get the username up front.
                beginRequestType = BeginRequestType.PROVIDE_USERNAME;
            }
        }

        this.emit('reset', beginRequestType);
    }

    addCharacter(unichar) {
        if (!this._entry.visible)
            return;

        this._entry.grab_key_focus();
        this._entry.clutter_text.insert_unichar(unichar);
    }

    begin(params) {
        params = Params.parse(params, {
            userName: null,
            hold: null,
        });

        this.hideLoginFailedNotification();
        this.updateSensitivity(false);

        let hold = params.hold;
        if (!hold)
            hold = new Batch.Hold();

        this._userVerifier.begin(params.userName, hold);
        this.verificationStatus = AuthPromptStatus.VERIFYING;
    }

    finish(onComplete) {
        if (!this._userVerifier.hasPendingMessages) {
            this._userVerifier.clear();
            onComplete();
            return;
        }

        let signalId = this._userVerifier.connect('no-more-messages', () => {
            this._userVerifier.disconnect(signalId);
            this._userVerifier.clear();
            onComplete();
        });
    }

    cancel() {
        if (this.verificationStatus === AuthPromptStatus.VERIFICATION_SUCCEEDED)
            return;

        if (this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) {
            this._cancelledRetries++;
            if (this._cancelledRetries > this._userVerifier.allowedFailures)
                this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED;
        } else {
            this.verificationStatus = AuthPromptStatus.VERIFICATION_CANCELLED;
        }

        this.reset();
    }
});
(uuay)gnome/�introspect.js�import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

const APP_ALLOWLIST = [
    'org.freedesktop.impl.portal.desktop.gtk',
    'org.freedesktop.impl.portal.desktop.gnome',
];

const INTROSPECT_DBUS_API_VERSION = 3;

import {loadInterfaceXML} from './fileUtils.js';
import {DBusSenderChecker} from './util.js';

const IntrospectDBusIface = loadInterfaceXML('org.gnome.Shell.Introspect');

export class IntrospectService {
    constructor() {
        this._dbusImpl =
            Gio.DBusExportedObject.wrapJSObject(IntrospectDBusIface, this);
        this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Introspect');
        Gio.DBus.session.own_name('org.gnome.Shell.Introspect',
            Gio.BusNameOwnerFlags.REPLACE,
            null, null);

        this._runningApplications = {};
        this._runningApplicationsDirty = true;
        this._activeApplication = null;
        this._activeApplicationDirty = true;
        this._animationsEnabled = true;

        this._appSystem = Shell.AppSystem.get_default();
        this._appSystem.connect('app-state-changed', () => {
            this._runningApplicationsDirty = true;
            this._syncRunningApplications();
        });

        let tracker = Shell.WindowTracker.get_default();
        tracker.connect('notify::focus-app', () => {
            this._activeApplicationDirty = true;
            this._syncRunningApplications();
        });

        tracker.connect('tracked-windows-changed',
            () => this._dbusImpl.emit_signal('WindowsChanged', null));

        this._syncRunningApplications();

        this._senderChecker = new DBusSenderChecker(APP_ALLOWLIST);

        this._settings = St.Settings.get();
        this._settings.connect('notify::enable-animations',
            this._syncAnimationsEnabled.bind(this));
        this._syncAnimationsEnabled();

        const monitorManager = global.backend.get_monitor_manager();
        monitorManager.connect('monitors-changed',
            this._syncScreenSize.bind(this));
        this._syncScreenSize();
    }

    _isStandaloneApp(app) {
        return app.get_windows().some(w => w.transient_for == null);
    }

    _getSandboxedAppId(app) {
        let ids = app.get_windows().map(w => w.get_sandboxed_app_id());
        return ids.find(id => id != null);
    }

    _syncRunningApplications() {
        let tracker = Shell.WindowTracker.get_default();
        let apps = this._appSystem.get_running();
        let seatName = 'seat0';
        let newRunningApplications = {};

        let newActiveApplication = null;
        let focusedApp = tracker.focus_app;

        for (let app of apps) {
            let appInfo = {};
            let isAppActive = focusedApp === app;

            if (!this._isStandaloneApp(app))
                continue;

            if (isAppActive) {
                appInfo['active-on-seats'] = new GLib.Variant('as', [seatName]);
                newActiveApplication = app.get_id();
            }

            let sandboxedAppId = this._getSandboxedAppId(app);
            if (sandboxedAppId)
                appInfo['sandboxed-app-id'] = new GLib.Variant('s', sandboxedAppId);

            newRunningApplications[app.get_id()] = appInfo;
        }

        if (this._runningApplicationsDirty ||
            (this._activeApplicationDirty &&
             this._activeApplication !== newActiveApplication)) {
            this._runningApplications = newRunningApplications;
            this._activeApplication = newActiveApplication;

            this._dbusImpl.emit_signal('RunningApplicationsChanged', null);
        }
        this._runningApplicationsDirty = false;
        this._activeApplicationDirty = false;
    }

    _isEligibleWindow(window) {
        if (window.is_override_redirect())
            return false;

        let type = window.get_window_type();
        return type === Meta.WindowType.NORMAL ||
            type === Meta.WindowType.DIALOG ||
            type === Meta.WindowType.MODAL_DIALOG ||
            type === Meta.WindowType.UTILITY;
    }

    async GetRunningApplicationsAsync(params, invocation) {
        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        invocation.return_value(new GLib.Variant('(a{sa{sv}})', [this._runningApplications]));
    }

    async GetWindowsAsync(params, invocation) {
        let focusWindow = global.display.get_focus_window();
        let apps = this._appSystem.get_running();
        let windowsList = {};

        try {
            await this._senderChecker.checkInvocation(invocation);
        } catch (e) {
            invocation.return_gerror(e);
            return;
        }

        for (let app of apps) {
            let windows = app.get_windows();
            for (let window of windows) {
                if (!this._isEligibleWindow(window))
                    continue;

                let windowId = window.get_id();
                let frameRect = window.get_frame_rect();
                let title = window.get_title();
                let wmClass = window.get_wm_class();
                let sandboxedAppId = window.get_sandboxed_app_id();

                windowsList[windowId] = {
                    'app-id': GLib.Variant.new('s', app.get_id()),
                    'client-type': GLib.Variant.new('u', window.get_client_type()),
                    'is-hidden': GLib.Variant.new('b', window.is_hidden()),
                    'has-focus': GLib.Variant.new('b', window === focusWindow),
                    'width': GLib.Variant.new('u', frameRect.width),
                    'height': GLib.Variant.new('u', frameRect.height),
                };

                // These properties may not be available for all windows:
                if (title != null)
                    windowsList[windowId]['title'] = GLib.Variant.new('s', title);

                if (wmClass != null)
                    windowsList[windowId]['wm-class'] = GLib.Variant.new('s', wmClass);

                if (sandboxedAppId != null) {
                    windowsList[windowId]['sandboxed-app-id'] =
                        GLib.Variant.new('s', sandboxedAppId);
                }
            }
        }
        invocation.return_value(new GLib.Variant('(a{ta{sv}})', [windowsList]));
    }

    _syncAnimationsEnabled() {
        let wasAnimationsEnabled = this._animationsEnabled;
        this._animationsEnabled = this._settings.enable_animations;
        if (wasAnimationsEnabled !== this._animationsEnabled) {
            let variant = new GLib.Variant('b', this._animationsEnabled);
            this._dbusImpl.emit_property_changed('AnimationsEnabled', variant);
        }
    }

    _syncScreenSize() {
        const oldScreenWidth = this._screenWidth;
        const oldScreenHeight = this._screenHeight;
        this._screenWidth = global.screen_width;
        this._screenHeight = global.screen_height;

        if (oldScreenWidth !== this._screenWidth ||
            oldScreenHeight !== this._screenHeight) {
            const variant = new GLib.Variant('(ii)',
                [this._screenWidth, this._screenHeight]);
            this._dbusImpl.emit_property_changed('ScreenSize', variant);
        }
    }

    get AnimationsEnabled() {
        return this._animationsEnabled;
    }

    get ScreenSize() {
        return [this._screenWidth, this._screenHeight];
    }

    get version() {
        return INTROSPECT_DBUS_API_VERSION;
    }
}
(uuay)ui/Bg�D-aA2)�TI��?>[4w6�L;�bz��HNpqf<yl.Rc
:]Q��7orF��5Z\K��90	�_imO�
E}PM�,`|3system.js�-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
import UPower from 'gi://UPowerGlib';

import * as SystemActions from '../../misc/systemActions.js';
import * as Main from '../main.js';
import * as PopupMenu from '../popupMenu.js';
import {PopupAnimation} from '../boxpointer.js';

import {QuickSettingsItem, QuickToggle, SystemIndicator} from '../quickSettings.js';
import {loadInterfaceXML} from '../../misc/fileUtils.js';

const BUS_NAME = 'org.freedesktop.UPower';
const OBJECT_PATH = '/org/freedesktop/UPower/devices/DisplayDevice';

const DisplayDeviceInterface = loadInterfaceXML('org.freedesktop.UPower.Device');
const PowerManagerProxy = Gio.DBusProxy.makeProxyWrapper(DisplayDeviceInterface);

const SHOW_BATTERY_PERCENTAGE = 'show-battery-percentage';

const PowerToggle = GObject.registerClass({
    Properties: {
        'fallback-icon-name': GObject.ParamSpec.string('fallback-icon-name', '', '',
            GObject.ParamFlags.READWRITE,
            ''),
    },
}, class PowerToggle extends QuickToggle {
    _init() {
        super._init({
            accessible_role: Atk.Role.PUSH_BUTTON,
        });

        this.add_style_class_name('power-item');

        this._proxy = new PowerManagerProxy(Gio.DBus.system, BUS_NAME, OBJECT_PATH,
            (proxy, error) => {
                if (error)
                    console.error(error.message);
                else
                    this._proxy.connect('g-properties-changed', () => this._sync());
                this._sync();
            });

        this.bind_property('fallback-icon-name',
            this._icon, 'fallback-icon-name',
            GObject.BindingFlags.SYNC_CREATE);

        this.connect('clicked', () => {
            const app = Shell.AppSystem.get_default().lookup_app('gnome-power-panel.desktop');
            Main.overview.hide();
            Main.panel.closeQuickSettings();
            app.activate();
        });

        Main.sessionMode.connect('updated', () => this._sessionUpdated());
        this._sessionUpdated();
        this._sync();
    }

    _sessionUpdated() {
        this.reactive = Main.sessionMode.allowSettings;
    }

    _sync() {
        // Do we have batteries or a UPS?
        this.visible = this._proxy.IsPresent;
        if (!this.visible)
            return;

        // The icons
        let chargingState = this._proxy.State === UPower.DeviceState.CHARGING
            ? '-charging' : '';
        let fillLevel = 10 * Math.floor(this._proxy.Percentage / 10);
        const charged =
            this._proxy.State === UPower.DeviceState.FULLY_CHARGED ||
            (this._proxy.State === UPower.DeviceState.CHARGING && fillLevel === 100);
        const icon = charged
            ? 'battery-level-100-charged-symbolic'
            : `battery-level-${fillLevel}${chargingState}-symbolic`;

        // Make sure we fall back to fallback-icon-name and not GThemedIcon's
        // default fallbacks
        const gicon = new Gio.ThemedIcon({
            name: icon,
            use_default_fallbacks: false,
        });

        const formatter = new Intl.NumberFormat(undefined, {style: 'percent'});
        this.set({
            title: formatter.format(this._proxy.Percentage / 100),
            fallback_icon_name: this._proxy.IconName,
            gicon,
        });
    }
});

const ScreenshotItem = GObject.registerClass(
class ScreenshotItem extends QuickSettingsItem {
    _init() {
        super._init({
            style_class: 'icon-button',
            can_focus: true,
            icon_name: 'screenshooter-symbolic',
            visible: !Main.sessionMode.isGreeter,
            accessible_name: _('Take Screenshot'),
        });

        this.connect('clicked', () => {
            const topMenu = Main.panel.statusArea.quickSettings.menu;
            const laters = global.compositor.get_laters();
            laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                Main.screenshotUI.open().catch(logError);
                return GLib.SOURCE_REMOVE;
            });
            topMenu.close(PopupAnimation.NONE);
        });
    }
});

const SettingsItem = GObject.registerClass(
class SettingsItem extends QuickSettingsItem {
    _init() {
        super._init({
            style_class: 'icon-button',
            can_focus: true,
            child: new St.Icon(),
        });

        this._settingsApp = Shell.AppSystem.get_default().lookup_app(
            'org.gnome.Settings.desktop');

        if (!this._settingsApp)
            console.warn('Missing required core component Settings, expect trouble…');

        this.child.gicon = this._settingsApp?.get_icon() ?? null;
        this.accessible_name = this._settingsApp?.get_name() ?? null;

        this.connect('clicked', () => {
            Main.overview.hide();
            Main.panel.closeQuickSettings();
            this._settingsApp.activate();
        });

        Main.sessionMode.connectObject('updated', () => this._sync(), this);
        this._sync();
    }

    _sync() {
        this.visible =
            this._settingsApp != null && Main.sessionMode.allowSettings;
    }
});

const ShutdownItem = GObject.registerClass(
class ShutdownItem extends QuickSettingsItem {
    _init() {
        super._init({
            style_class: 'icon-button',
            hasMenu: true,
            canFocus: true,
            icon_name: 'system-shutdown-symbolic',
            accessible_name: _('Power Off Menu'),
        });

        this._systemActions = new SystemActions.getDefault();
        this._items = [];

        this.menu.setHeader('system-shutdown-symbolic', C_('title', 'Power Off'));

        this._addSystemAction(_('Suspend'), 'can-suspend', () => {
            this._systemActions.activateSuspend();
            Main.panel.closeQuickSettings();
        });

        this._addSystemAction(_('Restart…'), 'can-restart', () => {
            this._systemActions.activateRestart();
            Main.panel.closeQuickSettings();
        });

        this._addSystemAction(_('Power Off…'), 'can-power-off', () => {
            this._systemActions.activatePowerOff();
            Main.panel.closeQuickSettings();
        });

        this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

        this._addSystemAction(_('Log Out…'), 'can-logout', () => {
            this._systemActions.activateLogout();
            Main.panel.closeQuickSettings();
        });

        this._addSystemAction(_('Switch User…'), 'can-switch-user', () => {
            this._systemActions.activateSwitchUser();
            Main.panel.closeQuickSettings();
        });

        // Whether shutdown is available or not depends on both lockdown
        // settings (disable-log-out) and Polkit policy - the latter doesn't
        // notify, so we update the item each time we become visible or
        // the lockdown setting changes, which should be close enough.
        this.connect('notify::mapped', () => {
            if (!this.mapped)
                return;

            this._systemActions.forceUpdate();
        });

        this.connect('clicked', () => this.menu.open());
        this.connect('popup-menu', () => this.menu.open());
    }

    _addSystemAction(label, propName, callback) {
        const item = this.menu.addAction(label, callback);
        this._items.push(item);

        this._systemActions.bind_property(propName,
            item, 'visible',
            GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE);
        item.connect('notify::visible', () => this._sync());
    }

    _sync() {
        this.visible = this._items.some(i => i.visible);
    }
});

const LockItem = GObject.registerClass(
class LockItem extends QuickSettingsItem {
    _init() {
        this._systemActions = new SystemActions.getDefault();

        super._init({
            style_class: 'icon-button',
            can_focus: true,
            icon_name: 'system-lock-screen-symbolic',
            accessible_name: C_('action', 'Lock Screen'),
        });

        this._systemActions.bind_property('can-lock-screen',
            this, 'visible',
            GObject.BindingFlags.DEFAULT |
            GObject.BindingFlags.SYNC_CREATE);

        this.connect('clicked',
            () => this._systemActions.activateLockScreen());
    }
});


const SystemItem = GObject.registerClass(
class SystemItem extends QuickSettingsItem {
    _init() {
        super._init({
            style_class: 'quick-settings-system-item',
            reactive: false,
        });

        this.child = new St.BoxLayout();

        this._powerToggle = new PowerToggle();
        this.child.add_child(this._powerToggle);

        this._laptopSpacer = new Clutter.Actor({x_expand: true});
        this._powerToggle.bind_property('visible',
            this._laptopSpacer, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        this.child.add_child(this._laptopSpacer);

        const screenshotItem = new ScreenshotItem();
        this.child.add_child(screenshotItem);

        const settingsItem = new SettingsItem();
        this.child.add_child(settingsItem);

        this._desktopSpacer = new Clutter.Actor({x_expand: true});
        this._powerToggle.bind_property('visible',
            this._desktopSpacer, 'visible',
            GObject.BindingFlags.INVERT_BOOLEAN |
            GObject.BindingFlags.SYNC_CREATE);
        this.child.add_child(this._desktopSpacer);

        const lockItem = new LockItem();
        this.child.add_child(lockItem);

        const shutdownItem = new ShutdownItem();
        this.child.add_child(shutdownItem);

        this.menu = shutdownItem.menu;
    }

    get powerToggle() {
        return this._powerToggle;
    }
});

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    _init() {
        super._init();

        this._desktopSettings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.interface',
        });
        this._desktopSettings.connectObject(
            `changed::${SHOW_BATTERY_PERCENTAGE}`, () => this._sync(), this);

        this._indicator = this._addIndicator();
        this._percentageLabel = new St.Label({
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this._percentageLabel);
        this.add_style_class_name('power-status');

        this._systemItem = new SystemItem();

        const {powerToggle} = this._systemItem;

        powerToggle.bind_property('title',
            this._percentageLabel, 'text',
            GObject.BindingFlags.SYNC_CREATE);

        powerToggle.connectObject(
            'notify::visible', () => this._sync(),
            'notify::gicon', () => this._sync(),
            'notify::fallback-icon-name', () => this._sync(),
            this);

        this.quickSettingsItems.push(this._systemItem);

        this._sync();
    }

    _sync() {
        const {powerToggle} = this._systemItem;
        if (powerToggle.visible) {
            this._indicator.set({
                gicon: powerToggle.gicon,
                fallback_icon_name: powerToggle.fallback_icon_name,
            });
            this._percentageLabel.visible =
                this._desktopSettings.get_boolean(SHOW_BATTERY_PERCENTAGE);
        } else {
            // If there's no battery, then we use the power icon.
            this._indicator.icon_name = 'system-shutdown-symbolic';
            this._percentageLabel.hide();
        }
    }
});
(uuay)popupMenu.jsq�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Shell from 'gi://Shell';
import St from 'gi://St';
import * as Signals from '../misc/signals.js';

import * as BoxPointer from './boxpointer.js';
import * as Main from './main.js';
import * as Params from '../misc/params.js';

/** @enum {number} */
export const Ornament = {
    NONE: 0,
    DOT: 1,
    CHECK: 2,
    HIDDEN: 3,
    NO_DOT: 4,
};

function isPopupMenuItemVisible(child) {
    if (child._delegate instanceof PopupMenuSection) {
        if (child._delegate.isEmpty())
            return false;
    }
    return child.visible;
}

/**
 * arrowIcon
 *
 * @param {St.Side} side - Side to which the arrow points.
 * @returns {St.Icon} a new arrow icon
 */
export function arrowIcon(side) {
    let iconName;
    switch (side) {
    case St.Side.TOP:
        iconName = 'pan-up-symbolic';
        break;
    case St.Side.RIGHT:
        iconName = 'pan-end-symbolic';
        break;
    case St.Side.BOTTOM:
        iconName = 'pan-down-symbolic';
        break;
    case St.Side.LEFT:
        iconName = 'pan-start-symbolic';
        break;
    }

    const arrow = new St.Icon({
        style_class: 'popup-menu-arrow',
        icon_name: iconName,
        accessible_role: Atk.Role.ARROW,
        y_expand: true,
        y_align: Clutter.ActorAlign.CENTER,
    });

    return arrow;
}

export const PopupBaseMenuItem = GObject.registerClass({
    Properties: {
        'active': GObject.ParamSpec.boolean(
            'active', 'active', 'active',
            GObject.ParamFlags.READWRITE,
            false),
        'sensitive': GObject.ParamSpec.boolean(
            'sensitive', 'sensitive', 'sensitive',
            GObject.ParamFlags.READWRITE,
            true),
    },
    Signals: {
        'activate': {param_types: [Clutter.Event.$gtype]},
    },
}, class PopupBaseMenuItem extends St.BoxLayout {
    _init(params) {
        params = Params.parse(params, {
            reactive: true,
            activate: true,
            hover: true,
            style_class: null,
            can_focus: true,
        });
        super._init({
            style_class: 'popup-menu-item',
            reactive: params.reactive,
            track_hover: params.reactive,
            can_focus: params.can_focus,
            accessible_role: Atk.Role.MENU_ITEM,
        });
        this._delegate = this;

        this._ornamentIcon = new St.Icon({style_class: 'popup-menu-ornament'});
        this.add_child(this._ornamentIcon);
        this.setOrnament(Ornament.HIDDEN);

        this._parent = null;
        this._active = false;
        this._activatable = params.reactive && params.activate;
        this._sensitive = true;

        this._clickAction = new Clutter.ClickAction({
            enabled: this._activatable,
        });
        this._clickAction.connect('clicked',
            () => this.activate(Clutter.get_current_event()));
        this._clickAction.connect('notify::pressed', () => {
            if (this._clickAction.pressed)
                this.add_style_pseudo_class('active');
            else
                this.remove_style_pseudo_class('active');
        });
        this.add_action(this._clickAction);

        if (!this._activatable)
            this.add_style_class_name('popup-inactive-menu-item');

        if (params.style_class)
            this.add_style_class_name(params.style_class);

        if (params.reactive && params.hover)
            this.bind_property('hover', this, 'active', GObject.BindingFlags.SYNC_CREATE);
    }

    get actor() {
        /* This is kept for compatibility with current implementation, and we
           don't want to warn here yet since PopupMenu depends on this */
        return this;
    }

    _getTopMenu() {
        if (this._parent)
            return this._parent._getTopMenu();
        else
            return this;
    }

    _setParent(parent) {
        this._parent = parent;
    }

    vfunc_key_press_event(event) {
        if (global.focus_manager.navigate_from_event(event))
            return Clutter.EVENT_STOP;

        if (!this._activatable)
            return super.vfunc_key_press_event(event);

        let state = event.get_state();

        // if user has a modifier down (except capslock and numlock)
        // then don't handle the key press here
        state &= ~Clutter.ModifierType.LOCK_MASK;
        state &= ~Clutter.ModifierType.MOD2_MASK;
        state &= Clutter.ModifierType.MODIFIER_MASK;

        if (state)
            return Clutter.EVENT_PROPAGATE;

        let symbol = event.get_key_symbol();
        if (symbol === Clutter.KEY_space || symbol === Clutter.KEY_Return) {
            this.activate(event);
            return Clutter.EVENT_STOP;
        }

        // Support wrapping navigation in the menu
        if (symbol === Clutter.KEY_Up || symbol === Clutter.KEY_Down) {
            const group = global.focus_manager.get_group(this);
            const direction = symbol === Clutter.KEY_Up
                ? St.DirectionType.UP
                : St.DirectionType.DOWN;
            if (group?.navigate_focus(this, direction, true))
                return Clutter.EVENT_STOP;
        }
        return Clutter.EVENT_PROPAGATE;
    }

    vfunc_key_focus_in() {
        super.vfunc_key_focus_in();
        this.active = true;
    }

    vfunc_key_focus_out() {
        super.vfunc_key_focus_out();
        this.active = false;
    }

    activate(event) {
        this.emit('activate', event);
    }

    get active() {
        return this._active;
    }

    set active(active) {
        let activeChanged = active !== this.active;
        if (activeChanged) {
            this._active = active;
            if (active) {
                this.add_style_class_name('selected');
                if (this.can_focus)
                    this.grab_key_focus();
            } else {
                this.remove_style_class_name('selected');
                // Remove the CSS active state if the user press the button and
                // while holding moves to another menu item, so we don't paint all items.
                // The correct behaviour would be to set the new item with the CSS
                // active state as well, but button-press-event is not triggered,
                // so we should track it in our own, which would involve some work
                // in the container
                this.remove_style_pseudo_class('active');
            }
            this.notify('active');
        }
    }

    syncSensitive() {
        let sensitive = this.sensitive;
        this.reactive = sensitive;
        this.can_focus = sensitive;
        this.notify('sensitive');
        return sensitive;
    }

    getSensitive() {
        const parentSensitive = this._parent?.sensitive ?? true;
        return this._activatable && this._sensitive && parentSensitive;
    }

    setSensitive(sensitive) {
        if (this._sensitive === sensitive)
            return;

        this._sensitive = sensitive;
        this.syncSensitive();
    }

    get sensitive() {
        return this.getSensitive();
    }

    set sensitive(sensitive) {
        this.setSensitive(sensitive);
    }

    setOrnament(ornament) {
        if (ornament === this._ornament)
            return;

        this._ornament = ornament;

        if (ornament === Ornament.DOT) {
            this._ornamentIcon.icon_name = 'ornament-dot-checked-symbolic';
            this.add_accessible_state(Atk.StateType.CHECKED);
        } else if (ornament === Ornament.NO_DOT) {
            this._ornamentIcon.icon_name = 'ornament-dot-unchecked-symbolic';
            this.remove_accessible_state(Atk.StateType.CHECKED);
        } else if (ornament === Ornament.CHECK) {
            this._ornamentIcon.icon_name = 'ornament-check-symbolic';
            this.add_accessible_state(Atk.StateType.CHECKED);
        } else if (ornament === Ornament.NONE || ornament === Ornament.HIDDEN) {
            this._ornamentIcon.icon_name = '';
            this.remove_accessible_state(Atk.StateType.CHECKED);
        }

        this._ornamentIcon.visible = ornament !== Ornament.HIDDEN;
        this._updateOrnamentStyle();
    }

    _updateOrnamentStyle() {
        if (this._ornament === Ornament.CHECK || this._ornament === Ornament.NONE)
            this.add_style_class_name('popup-ornamented-menu-item');
        else
            this.remove_style_class_name('popup-ornamented-menu-item');
    }
});

export const PopupMenuItem = GObject.registerClass(
class PopupMenuItem extends PopupBaseMenuItem {
    _init(text, params) {
        super._init(params);

        this.label = new St.Label({
            text,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this.label);
        this.label_actor = this.label;
    }
});


export const PopupSeparatorMenuItem = GObject.registerClass(
class PopupSeparatorMenuItem extends PopupBaseMenuItem {
    _init(text) {
        super._init({
            style_class: 'popup-separator-menu-item',
            reactive: false,
            can_focus: false,
        });

        this.label = new St.Label({text: text || ''});
        this.add_child(this.label);
        this.label_actor = this.label;

        this.label.connect('notify::text',
            this._syncVisibility.bind(this));
        this._syncVisibility();

        this._separator = new St.Widget({
            style_class: 'popup-separator-menu-item-separator',
            x_expand: true,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this._separator);
    }

    _syncVisibility() {
        this.label.visible = this.label.text !== '';
    }
});

export const Switch = GObject.registerClass({
    Properties: {
        'state': GObject.ParamSpec.boolean(
            'state', 'state', 'state',
            GObject.ParamFlags.READWRITE,
            false),
    },
}, class Switch extends St.Bin {
    _init(state) {
        this._state = false;

        const box = new St.BoxLayout({
            x_expand: true,
            y_expand: true,
        });

        super._init({
            style_class: 'toggle-switch',
            child: box,
            accessible_role: Atk.Role.CHECK_BOX,
            state,
        });

        this._onIcon = new St.Icon({
            icon_name: 'switch-on-symbolic',
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(this._onIcon);

        this._offIcon = new St.Icon({
            icon_name: 'switch-off-symbolic',
            x_expand: true,
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(this._offIcon);

        this._a11ySettings = new Gio.Settings({
            schema_id: 'org.gnome.desktop.a11y.interface',
        });

        this._a11ySettings.connectObject('changed::show-status-shapes',
            () => this._updateIconOpacity(),
            this);
        this.connect('notify::state',
            () => this._updateIconOpacity());
        this._updateIconOpacity();
    }

    _updateIconOpacity() {
        const activeOpacity = this._a11ySettings.get_boolean('show-status-shapes')
            ? 255. : 0.;

        this._onIcon.opacity = this.state
            ? activeOpacity : 0.;
        this._offIcon.opacity = this.state
            ? 0. : activeOpacity;
    }

    get state() {
        return this._state;
    }

    set state(state) {
        if (this._state === state)
            return;

        if (state)
            this.add_style_pseudo_class('checked');
        else
            this.remove_style_pseudo_class('checked');

        this._state = state;
        this.notify('state');
    }

    toggle() {
        this.state = !this.state;
    }
});

export const PopupSwitchMenuItem = GObject.registerClass({
    Signals: {'toggled': {param_types: [GObject.TYPE_BOOLEAN]}},
}, class PopupSwitchMenuItem extends PopupBaseMenuItem {
    _init(text, active, params) {
        super._init(params);

        this.label = new St.Label({
            text,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._switch = new Switch(active);

        this.accessible_role = Atk.Role.CHECK_MENU_ITEM;
        this.checkAccessibleState();
        this.label_actor = this.label;

        this.add_child(this.label);

        this._statusBin = new St.Bin({
            x_align: Clutter.ActorAlign.END,
            x_expand: true,
        });
        this.add_child(this._statusBin);

        this._statusLabel = new St.Label({
            text: '',
            style_class: 'popup-status-menu-item',
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._statusBin.child = this._switch;
    }

    setStatus(text) {
        if (text != null) {
            this._statusLabel.text = text;
            this._statusBin.child = this._statusLabel;
            this.reactive = false;
            this.accessible_role = Atk.Role.MENU_ITEM;
        } else {
            this._statusBin.child = this._switch;
            this.reactive = true;
            this.accessible_role = Atk.Role.CHECK_MENU_ITEM;
        }
        this.checkAccessibleState();
    }

    activate(event) {
        if (this._switch.mapped)
            this.toggle();

        // we allow pressing space to toggle the switch
        // without closing the menu
        if (event.type() === Clutter.EventType.KEY_PRESS &&
            event.get_key_symbol() === Clutter.KEY_space)
            return;

        super.activate(event);
    }

    toggle() {
        this._switch.toggle();
        this.emit('toggled', this._switch.state);
        this.checkAccessibleState();
    }

    get state() {
        return this._switch.state;
    }

    setToggleState(state) {
        this._switch.state = state;
        this.checkAccessibleState();
    }

    checkAccessibleState() {
        switch (this.accessible_role) {
        case Atk.Role.CHECK_MENU_ITEM:
            if (this._switch.state)
                this.add_accessible_state(Atk.StateType.CHECKED);
            else
                this.remove_accessible_state(Atk.StateType.CHECKED);
            break;
        default:
            this.remove_accessible_state(Atk.StateType.CHECKED);
        }
    }
});

export const PopupImageMenuItem = GObject.registerClass(
class PopupImageMenuItem extends PopupBaseMenuItem {
    _init(text, icon, params) {
        super._init(params);

        this._icon = new St.Icon({
            style_class: 'popup-menu-icon',
            x_align: Clutter.ActorAlign.END,
        });
        this.add_child(this._icon);
        this.label = new St.Label({
            text,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this.label);
        this.label_actor = this.label;

        this.set_child_above_sibling(this._ornamentIcon, this.label);

        this.setIcon(icon);
    }

    setIcon(icon) {
        // The 'icon' parameter can be either a Gio.Icon or a string.
        if (icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon))
            this._icon.gicon = icon;
        else
            this._icon.icon_name = icon;
    }

    _updateOrnamentStyle() {
        // we move the ornament after the label, so we don't need
        // additional padding regardless of ornament visibility
    }
});

export class PopupMenuBase extends Signals.EventEmitter {
    constructor(sourceActor, styleClass) {
        super();

        if (this.constructor === PopupMenuBase)
            throw new TypeError(`Cannot instantiate abstract class ${this.constructor.name}`);

        this.sourceActor = sourceActor;
        this.focusActor = sourceActor;
        this._parent = null;

        this.box = new St.BoxLayout({
            vertical: true,
            x_expand: true,
            y_expand: true,
        });

        if (styleClass !== undefined)
            this.box.style_class = styleClass;
        this.length = 0;

        this.isOpen = false;

        this._activeMenuItem = null;
        this._settingsActions = { };

        this._sensitive = true;

        Main.sessionMode.connectObject('updated', () => this._sessionUpdated(), this);
    }

    _getTopMenu() {
        if (this._parent)
            return this._parent._getTopMenu();
        else
            return this;
    }

    _setParent(parent) {
        this._parent = parent;
    }

    getSensitive() {
        const parentSensitive = this._parent?.sensitive ?? true;
        return this._sensitive && parentSensitive;
    }

    setSensitive(sensitive) {
        this._sensitive = sensitive;
        this.emit('notify::sensitive');
    }

    get sensitive() {
        return this.getSensitive();
    }

    set sensitive(sensitive) {
        this.setSensitive(sensitive);
    }

    _sessionUpdated() {
        this._setSettingsVisibility(Main.sessionMode.allowSettings);
        this.close();
    }

    addAction(title, callback, icon) {
        let menuItem;
        if (icon !== undefined)
            menuItem = new PopupImageMenuItem(title, icon);
        else
            menuItem = new PopupMenuItem(title);

        this.addMenuItem(menuItem);
        menuItem.connect('activate', (o, event) => {
            callback(event);
        });

        return menuItem;
    }

    addSettingsAction(title, desktopFile) {
        let menuItem = this.addAction(title, () => {
            let app = Shell.AppSystem.get_default().lookup_app(desktopFile);

            if (!app) {
                log(`Settings panel for desktop file ${desktopFile} could not be loaded!`);
                return;
            }

            Main.overview.hide();
            Main.panel.closeQuickSettings();
            app.activate();
        });

        menuItem.visible = Main.sessionMode.allowSettings;
        this._settingsActions[desktopFile] = menuItem;

        return menuItem;
    }

    _setSettingsVisibility(visible) {
        for (let id in this._settingsActions) {
            let item = this._settingsActions[id];
            item.visible = visible;
        }
    }

    isEmpty() {
        let hasVisibleChildren = this.box.get_children().some(child => {
            if (child._delegate instanceof PopupSeparatorMenuItem)
                return false;
            return isPopupMenuItemVisible(child);
        });

        return !hasVisibleChildren;
    }

    itemActivated(animate) {
        if (animate === undefined)
            animate = BoxPointer.PopupAnimation.FULL;

        this._getTopMenu().close(animate);
    }

    _subMenuActiveChanged(submenu, submenuItem) {
        if (this._activeMenuItem && this._activeMenuItem !== submenuItem)
            this._activeMenuItem.active = false;
        this._activeMenuItem = submenuItem;
        this.emit('active-changed', submenuItem);
    }

    _connectItemSignals(menuItem) {
        menuItem.connectObject(
            'notify::active', () => {
                const {active} = menuItem;
                if (active && this._activeMenuItem !== menuItem) {
                    if (this._activeMenuItem)
                        this._activeMenuItem.active = false;
                    this._activeMenuItem = menuItem;
                    this.emit('active-changed', menuItem);
                } else if (!active && this._activeMenuItem === menuItem) {
                    this._activeMenuItem = null;
                    this.emit('active-changed', null);
                }
            },
            'notify::sensitive', () => {
                const {sensitive} = menuItem;
                if (!sensitive && this._activeMenuItem === menuItem) {
                    if (!this.actor.navigate_focus(menuItem.actor,
                        St.DirectionType.TAB_FORWARD, true))
                        this.actor.grab_key_focus();
                } else if (sensitive && this._activeMenuItem === null) {
                    if (global.stage.get_key_focus() === this.actor)
                        menuItem.actor.grab_key_focus();
                }
            },
            'activate', () => {
                this.emit('activate', menuItem);
                this.itemActivated(BoxPointer.PopupAnimation.FULL);
            }, GObject.ConnectFlags.AFTER,
            'destroy', () => {
                if (menuItem === this._activeMenuItem)
                    this._activeMenuItem = null;
            }, this);

        this.connectObject('notify::sensitive',
            () => menuItem.syncSensitive(), menuItem);
    }

    _updateSeparatorVisibility(menuItem) {
        if (menuItem.label.text)
            return;

        let children = this.box.get_children();

        let index = children.indexOf(menuItem.actor);

        if (index < 0)
            return;

        let childBeforeIndex = index - 1;

        while (childBeforeIndex >= 0 && !isPopupMenuItemVisible(children[childBeforeIndex]))
            childBeforeIndex--;

        if (childBeforeIndex < 0 ||
            children[childBeforeIndex]._delegate instanceof PopupSeparatorMenuItem) {
            menuItem.actor.hide();
            return;
        }

        let childAfterIndex = index + 1;

        while (childAfterIndex < children.length && !isPopupMenuItemVisible(children[childAfterIndex]))
            childAfterIndex++;

        if (childAfterIndex >= children.length ||
            children[childAfterIndex]._delegate instanceof PopupSeparatorMenuItem) {
            menuItem.actor.hide();
            return;
        }

        menuItem.show();
    }

    moveMenuItem(menuItem, position) {
        let items = this._getMenuItems();
        let i = 0;

        while (i < items.length && position > 0) {
            if (items[i] !== menuItem)
                position--;
            i++;
        }

        if (i < items.length) {
            if (items[i] !== menuItem)
                this.box.set_child_below_sibling(menuItem.actor, items[i].actor);
        } else {
            this.box.set_child_above_sibling(menuItem.actor, null);
        }
    }

    addMenuItem(menuItem, position) {
        let beforeItem = null;
        if (position === undefined) {
            this.box.add_child(menuItem.actor);
        } else {
            let items = this._getMenuItems();
            if (position < items.length) {
                beforeItem = items[position].actor;
                this.box.insert_child_below(menuItem.actor, beforeItem);
            } else {
                this.box.add_child(menuItem.actor);
            }
        }

        if (menuItem instanceof PopupMenuSection) {
            menuItem.connectObject(
                'active-changed', this._subMenuActiveChanged.bind(this),
                'destroy', () => this.length--, this);

            this.connectObject(
                'open-state-changed', (self, open) => {
                    if (open)
                        menuItem.open();
                    else
                        menuItem.close();
                },
                'menu-closed', () => menuItem.emit('menu-closed'),
                'notify::sensitive', () => menuItem.emit('notify::sensitive'),
                menuItem);
        } else if (menuItem instanceof PopupSubMenuMenuItem) {
            if (beforeItem == null)
                this.box.add_child(menuItem.menu.actor);
            else
                this.box.insert_child_below(menuItem.menu.actor, beforeItem);

            this._connectItemSignals(menuItem);
            menuItem.menu.connectObject('active-changed',
                this._subMenuActiveChanged.bind(this), this);
            this.connectObject('menu-closed', () => {
                menuItem.menu.close(BoxPointer.PopupAnimation.NONE);
            }, menuItem);
        } else if (menuItem instanceof PopupSeparatorMenuItem) {
            this._connectItemSignals(menuItem);

            // updateSeparatorVisibility needs to get called any time the
            // separator's adjacent siblings change visibility or position.
            // open-state-changed isn't exactly that, but doing it in more
            // precise ways would require a lot more bookkeeping.
            this.connectObject('open-state-changed', () => {
                this._updateSeparatorVisibility(menuItem);
            }, menuItem);
        } else if (menuItem instanceof PopupBaseMenuItem) {
            this._connectItemSignals(menuItem);
        } else {
            throw TypeError('Invalid argument to PopupMenuBase.addMenuItem()');
        }

        menuItem._setParent(this);

        this.length++;
    }

    _getMenuItems() {
        return this.box.get_children().map(a => a._delegate).filter(item => {
            return item instanceof PopupBaseMenuItem || item instanceof PopupMenuSection;
        });
    }

    get firstMenuItem() {
        let items = this._getMenuItems();
        if (items.length)
            return items[0];
        else
            return null;
    }

    get numMenuItems() {
        return this._getMenuItems().length;
    }

    removeAll() {
        let children = this._getMenuItems();
        for (let i = 0; i < children.length; i++) {
            let item = children[i];
            item.destroy();
        }
    }

    toggle() {
        if (this.isOpen)
            this.close(BoxPointer.PopupAnimation.FULL);
        else
            this.open(BoxPointer.PopupAnimation.FULL);
    }

    destroy() {
        this.close();
        this.removeAll();
        this.actor.destroy();

        this.emit('destroy');

        Main.sessionMode.disconnectObject(this);
    }
}

export class PopupMenu extends PopupMenuBase {
    constructor(sourceActor, arrowAlignment, arrowSide) {
        super(sourceActor, 'popup-menu-content');

        this._arrowAlignment = arrowAlignment;
        this._arrowSide = arrowSide;

        this._boxPointer = new BoxPointer.BoxPointer(arrowSide);
        this.actor = this._boxPointer;
        this.actor._delegate = this;
        this.actor.style_class = 'popup-menu-boxpointer';

        this._boxPointer.bin.set_child(this.box);
        this.actor.add_style_class_name('popup-menu');

        global.focus_manager.add_group(this.actor);
        this.actor.reactive = true;

        if (this.sourceActor) {
            this.sourceActor.connectObject(
                'key-press-event', this._onKeyPress.bind(this),
                'notify::mapped', () => {
                    if (!this.sourceActor.mapped)
                        this.close();
                }, this);
        }

        this._systemModalOpenedId = 0;
        this._openedSubMenu = null;
    }

    _setOpenedSubMenu(submenu) {
        if (this._openedSubMenu)
            this._openedSubMenu.close(true);

        this._openedSubMenu = submenu;
    }

    _onKeyPress(actor, event) {
        // Disable toggling the menu by keyboard
        // when it cannot be toggled by pointer
        if (!actor.reactive)
            return Clutter.EVENT_PROPAGATE;

        let navKey;
        switch (this._boxPointer.arrowSide) {
        case St.Side.TOP:
            navKey = Clutter.KEY_Down;
            break;
        case St.Side.BOTTOM:
            navKey = Clutter.KEY_Up;
            break;
        case St.Side.LEFT:
            navKey = Clutter.KEY_Right;
            break;
        case St.Side.RIGHT:
            navKey = Clutter.KEY_Left;
            break;
        }

        let state = event.get_state();

        // if user has a modifier down (except capslock and numlock)
        // then don't handle the key press here
        state &= ~Clutter.ModifierType.LOCK_MASK;
        state &= ~Clutter.ModifierType.MOD2_MASK;
        state &= Clutter.ModifierType.MODIFIER_MASK;

        if (state)
            return Clutter.EVENT_PROPAGATE;

        let symbol = event.get_key_symbol();

        if (symbol === Clutter.KEY_space || symbol === Clutter.KEY_Return) {
            this.toggle();
            return Clutter.EVENT_STOP;
        } else if (symbol === navKey) {
            if (!this.isOpen)
                this.toggle();
            this.actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
            return Clutter.EVENT_STOP;
        } else {
            return Clutter.EVENT_PROPAGATE;
        }
    }

    setArrowOrigin(origin) {
        this._boxPointer.setArrowOrigin(origin);
    }

    setSourceAlignment(alignment) {
        this._boxPointer.setSourceAlignment(alignment);
    }

    open(animate) {
        if (this.isOpen)
            return;

        if (this.isEmpty())
            return;

        if (!this._systemModalOpenedId) {
            this._systemModalOpenedId =
                Main.layoutManager.connect('system-modal-opened', () => this.close());
        }

        this.isOpen = true;

        this._boxPointer.setPosition(this.sourceActor, this._arrowAlignment);
        this._boxPointer.open(animate);

        this.actor.get_parent().set_child_above_sibling(this.actor, null);

        this.emit('open-state-changed', true);
    }

    close(animate) {
        if (this._activeMenuItem)
            this._activeMenuItem.active = false;

        if (this._boxPointer.visible) {
            this._boxPointer.close(animate, () => {
                this.emit('menu-closed');
            });
        }

        if (!this.isOpen)
            return;

        this.isOpen = false;
        this.emit('open-state-changed', false);
    }

    destroy() {
        this.sourceActor?.disconnectObject(this);

        if (this._systemModalOpenedId)
            Main.layoutManager.disconnect(this._systemModalOpenedId);
        this._systemModalOpenedId = 0;

        super.destroy();
    }
}

export class PopupDummyMenu extends Signals.EventEmitter {
    constructor(sourceActor) {
        super();

        this.sourceActor = sourceActor;
        this.actor = sourceActor;
        this.actor._delegate = this;
    }

    getSensitive() {
        return true;
    }

    get sensitive() {
        return this.getSensitive();
    }

    open() {
        if (this.isOpen)
            return;
        this.isOpen = true;
        this.emit('open-state-changed', true);
    }

    close() {
        if (!this.isOpen)
            return;
        this.isOpen = false;
        this.emit('open-state-changed', false);
    }

    toggle() {}

    destroy() {
        this.emit('destroy');
    }
}

export class PopupSubMenu extends PopupMenuBase {
    constructor(sourceActor, sourceArrow) {
        super(sourceActor);

        this._arrow = sourceArrow;

        // Since a function of a submenu might be to provide a "More.." expander
        // with long content, we make it scrollable - the scrollbar will only take
        // effect if a CSS max-height is set on the top menu.
        this.actor = new St.ScrollView({
            style_class: 'popup-sub-menu',
            vscrollbar_policy: St.PolicyType.NEVER,
            child: this.box,
        });

        this.actor._delegate = this;
        this.actor.clip_to_allocation = true;
        this.actor.connect('key-press-event', this._onKeyPressEvent.bind(this));
        this.actor.hide();
    }

    _needsScrollbar() {
        let topMenu = this._getTopMenu();
        let [, topNaturalHeight] = topMenu.actor.get_preferred_height(-1);
        let topThemeNode = topMenu.actor.get_theme_node();

        let topMaxHeight = topThemeNode.get_max_height();
        return topMaxHeight >= 0 && topNaturalHeight >= topMaxHeight;
    }

    getSensitive() {
        return this._sensitive && this.sourceActor.sensitive;
    }

    get sensitive() {
        return this.getSensitive();
    }

    open(animate) {
        if (this.isOpen)
            return;

        if (this.isEmpty())
            return;

        this.isOpen = true;
        this.emit('open-state-changed', true);

        this.actor.show();

        let needsScrollbar = this._needsScrollbar();

        // St.ScrollView always requests space horizontally for a possible vertical
        // scrollbar if in AUTOMATIC mode. Doing better would require implementation
        // of width-for-height in St.BoxLayout and St.ScrollView. This looks bad
        // when we *don't* need it, so turn off the scrollbar when that's true.
        // Dynamic changes in whether we need it aren't handled properly.
        this.actor.vscrollbar_policy =
            needsScrollbar ? St.PolicyType.AUTOMATIC : St.PolicyType.NEVER;

        if (needsScrollbar)
            this.actor.add_style_pseudo_class('scrolled');
        else
            this.actor.remove_style_pseudo_class('scrolled');

        // It looks funny if we animate with a scrollbar (at what point is
        // the scrollbar added?) so just skip that case
        if (animate && needsScrollbar)
            animate = false;

        let targetAngle = this.actor.text_direction === Clutter.TextDirection.RTL ? -90 : 90;

        const duration = animate ? 250 : 0;
        let [, naturalHeight] = this.actor.get_preferred_height(-1);
        this.actor.height = 0;
        this.actor.ease({
            height: naturalHeight,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_EXPO,
            onComplete: () => this.actor.set_height(-1),
        });
        this._arrow.ease({
            rotation_angle_z: targetAngle,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_EXPO,
        });
    }

    close(animate) {
        if (!this.isOpen)
            return;

        this.isOpen = false;
        this.emit('open-state-changed', false);

        if (this._activeMenuItem)
            this._activeMenuItem.active = false;

        if (animate && this._needsScrollbar())
            animate = false;

        const duration = animate ? 250 : 0;
        this.actor.ease({
            height: 0,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_EXPO,
            onComplete: () => {
                this.actor.hide();
                this.actor.set_height(-1);
            },
        });
        this._arrow.ease({
            rotation_angle_z: 0,
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_EXPO,
        });
    }

    _onKeyPressEvent(actor, event) {
        // Move focus back to parent menu if the user types Left.

        if (this.isOpen && event.get_key_symbol() === Clutter.KEY_Left) {
            this.close(BoxPointer.PopupAnimation.FULL);
            this.sourceActor._delegate.active = true;
            return Clutter.EVENT_STOP;
        }

        return Clutter.EVENT_PROPAGATE;
    }
}

/**
 * PopupMenuSection:
 *
 * A section of a PopupMenu which is handled like a submenu
 * (you can add and remove items, you can destroy it, you
 * can add it to another menu), but is completely transparent
 * to the user
 */
export class PopupMenuSection extends PopupMenuBase {
    constructor() {
        super();

        this.actor = this.box;
        this.actor._delegate = this;
        this.isOpen = true;

        this.actor.add_style_class_name('popup-menu-section');
    }

    // deliberately ignore any attempt to open() or close(), but emit the
    // corresponding signal so children can still pick it up
    open() {
        this.emit('open-state-changed', true);
    }

    close() {
        this.emit('open-state-changed', false);
    }
}

export const PopupSubMenuMenuItem = GObject.registerClass(
class PopupSubMenuMenuItem extends PopupBaseMenuItem {
    _init(text, wantIcon) {
        super._init();

        this.add_style_class_name('popup-submenu-menu-item');

        if (wantIcon) {
            this.icon = new St.Icon({style_class: 'popup-menu-icon'});
            this.add_child(this.icon);
        }

        this.label = new St.Label({
            text,
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this.add_child(this.label);
        this.label_actor = this.label;

        let expander = new St.Bin({
            style_class: 'popup-menu-item-expander',
            x_expand: true,
        });
        this.add_child(expander);

        this._triangle = arrowIcon(St.Side.RIGHT);
        this._triangle.pivot_point = new Graphene.Point({x: 0.5, y: 0.6});

        this._triangleBin = new St.Widget({
            y_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._triangleBin.add_child(this._triangle);

        this.add_child(this._triangleBin);
        this.add_accessible_state(Atk.StateType.EXPANDABLE);

        this.menu = new PopupSubMenu(this, this._triangle);
        this.menu.connect('open-state-changed', this._subMenuOpenStateChanged.bind(this));
        this.connect('destroy', () => this.menu.destroy());
    }

    _setParent(parent) {
        super._setParent(parent);
        this.menu._setParent(parent);
    }

    syncSensitive() {
        let sensitive = super.syncSensitive();
        this._triangle.visible = sensitive;
        if (!sensitive)
            this.menu.close(false);
    }

    _subMenuOpenStateChanged(menu, open) {
        if (open) {
            this.add_style_pseudo_class('open');
            this._getTopMenu()._setOpenedSubMenu(this.menu);
            this.add_accessible_state(Atk.StateType.EXPANDED);
            this.add_style_pseudo_class('checked');
        } else {
            this.remove_style_pseudo_class('open');
            this._getTopMenu()._setOpenedSubMenu(null);
            this.remove_accessible_state(Atk.StateType.EXPANDED);
            this.remove_style_pseudo_class('checked');
        }
    }

    setSubmenuShown(open) {
        if (open)
            this.menu.open(BoxPointer.PopupAnimation.FULL);
        else
            this.menu.close(BoxPointer.PopupAnimation.FULL);
    }

    _setOpenState(open) {
        this.setSubmenuShown(open);
    }

    _getOpenState() {
        return this.menu.isOpen;
    }

    vfunc_key_press_event(event) {
        let symbol = event.get_key_symbol();

        if (symbol === Clutter.KEY_Right) {
            this._setOpenState(true);
            this.menu.actor.navigate_focus(null, St.DirectionType.DOWN, false);
            return Clutter.EVENT_STOP;
        } else if (symbol === Clutter.KEY_Left && this._getOpenState()) {
            this._setOpenState(false);
            return Clutter.EVENT_STOP;
        }

        return super.vfunc_key_press_event(event);
    }

    activate(_event) {
        this._setOpenState(!this._getOpenState());
    }
});

/* Basic implementation of a menu manager.
 * Call addMenu to add menus
 */
export class PopupMenuManager {
    constructor(owner, grabParams) {
        this._grabParams = Params.parse(grabParams,
            {actionMode: Shell.ActionMode.POPUP});
        this._menus = [];
    }

    addMenu(menu, position) {
        if (this._menus.includes(menu))
            return;

        menu.connectObject(
            'open-state-changed', this._onMenuOpenState.bind(this),
            'destroy', () => this.removeMenu(menu), this);
        menu.actor.connectObject('captured-event',
            this._onCapturedEvent.bind(this), this);

        if (position === undefined)
            this._menus.push(menu);
        else
            this._menus.splice(position, 0, menu);
    }

    removeMenu(menu) {
        if (menu === this.activeMenu) {
            Main.popModal(this._grab);
            this._grab = null;

            if (this._keyFocusId) {
                global.stage.disconnect(this._keyFocusId);
                delete this._keyFocusId;
            }
        }

        const position = this._menus.indexOf(menu);
        if (position === -1) // not a menu we manage
            return;

        menu.disconnectObject(this);
        menu.actor.disconnectObject(this);

        this._menus.splice(position, 1);
    }

    ignoreRelease() {
    }

    _onMenuOpenState(menu, open) {
        if (open && this.activeMenu === menu)
            return;

        if (open) {
            const oldMenu = this.activeMenu;
            const oldGrab = this._grab;
            this._grab = Main.pushModal(menu.actor, this._grabParams);
            this.activeMenu = menu;
            oldMenu?.close(BoxPointer.PopupAnimation.FADE);
            if (oldGrab)
                Main.popModal(oldGrab);

            if (!this._keyFocusId) {
                this._keyFocusId =
                    global.stage.connect('notify::key-focus', () => {
                        if (!this.activeMenu)
                            return;

                        let actor = global.stage.get_key_focus();
                        let newMenu = this._findMenuForSource(actor);

                        if (newMenu)
                            this._changeMenu(newMenu);
                    });
            }
        } else if (this.activeMenu === menu) {
            this.activeMenu = null;
            Main.popModal(this._grab);
            this._grab = null;

            if (this._keyFocusId) {
                global.stage.disconnect(this._keyFocusId);
                delete this._keyFocusId;
            }
        }
    }

    _changeMenu(newMenu) {
        newMenu.open(this.activeMenu
            ? BoxPointer.PopupAnimation.FADE
            : BoxPointer.PopupAnimation.FULL);
    }

    _onCapturedEvent(actor, event) {
        let menu = actor._delegate;
        const targetActor = global.stage.get_event_actor(event);

        if (event.type() === Clutter.EventType.KEY_PRESS) {
            let symbol = event.get_key_symbol();
            if (symbol === Clutter.KEY_Down &&
                global.stage.get_key_focus() === menu.actor) {
                actor.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
                return Clutter.EVENT_STOP;
            } else if (symbol === Clutter.KEY_Escape && menu.isOpen) {
                menu.close(BoxPointer.PopupAnimation.FULL);
                return Clutter.EVENT_STOP;
            }
        } else if (event.type() === Clutter.EventType.ENTER &&
                   (event.get_flags() & Clutter.EventFlags.FLAG_GRAB_NOTIFY) === 0) {
            let hoveredMenu = this._findMenuForSource(targetActor);

            if (hoveredMenu && hoveredMenu !== menu)
                this._changeMenu(hoveredMenu);
        } else if ((event.type() === Clutter.EventType.BUTTON_PRESS ||
                    event.type() === Clutter.EventType.TOUCH_BEGIN) &&
                   !actor.contains(targetActor)) {
            menu.close(BoxPointer.PopupAnimation.FULL);
        }

        return Clutter.EVENT_PROPAGATE;
    }

    _findMenuForSource(source) {
        while (source) {
            let actor = source;
            const menu = this._menus.find(m => m.sourceActor === actor);
            if (menu)
                return menu;
            source = source.get_parent();
        }

        return null;
    }

    _closeMenu(isUser, menu) {
        // If this isn't a user action, we called close()
        // on the BoxPointer ourselves, so we shouldn't
        // reanimate.
        if (isUser)
            menu.close(BoxPointer.PopupAnimation.FULL);
    }
}
(uuay)quickSettings.jsJkimport Atk from 'gi://Atk';
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Graphene from 'gi://Graphene';
import Meta from 'gi://Meta';
import Pango from 'gi://Pango';
import St from 'gi://St';

import * as Main from './main.js';
import * as PopupMenu from './popupMenu.js';
import {Slider} from './slider.js';

import {PopupAnimation} from './boxpointer.js';

const DIM_BRIGHTNESS = -0.4;
const POPUP_ANIMATION_TIME = 400;
const MENU_BUTTON_BRIGHTNESS = 0.1;

export const QuickSettingsItem = GObject.registerClass({
    Properties: {
        'has-menu': GObject.ParamSpec.boolean(
            'has-menu', 'has-menu', 'has-menu',
            GObject.ParamFlags.READWRITE |
            GObject.ParamFlags.CONSTRUCT_ONLY,
            false),
    },
}, class QuickSettingsItem extends St.Button {
    _init(params) {
        super._init(params);

        if (this.hasMenu) {
            this.menu = new QuickToggleMenu(this);
            this.menu.actor.hide();

            this._menuManager = new PopupMenu.PopupMenuManager(this);
            this._menuManager.addMenu(this.menu);
        }
    }
});

export const QuickToggle = GObject.registerClass({
    Properties: {
        'title': GObject.ParamSpec.string('title', '', '',
            GObject.ParamFlags.READWRITE,
            null),
        'subtitle': GObject.ParamSpec.string('subtitle', '', '',
            GObject.ParamFlags.READWRITE,
            null),
        'icon-name': GObject.ParamSpec.override('icon-name', St.Button),
        'gicon': GObject.ParamSpec.object('gicon', '', '',
            GObject.ParamFlags.READWRITE,
            Gio.Icon),
    },
}, class QuickToggle extends QuickSettingsItem {
    _init(params) {
        super._init({
            style_class: 'quick-toggle button',
            accessible_role: Atk.Role.TOGGLE_BUTTON,
            can_focus: true,
            ...params,
        });

        this._box = new St.BoxLayout({x_expand: true});
        this.set_child(this._box);

        const iconProps = {};
        if (this.gicon)
            iconProps['gicon'] = this.gicon;
        if (this.iconName)
            iconProps['icon-name'] = this.iconName;

        this._icon = new St.Icon({
            style_class: 'quick-toggle-icon',
            x_expand: false,
            ...iconProps,
        });
        this._box.add_child(this._icon);

        // bindings are in the "wrong" direction, so we
        // pick up StIcon's linking of the two properties
        this._icon.bind_property('icon-name',
            this, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE |
            GObject.BindingFlags.BIDIRECTIONAL);
        this._icon.bind_property('gicon',
            this, 'gicon',
            GObject.BindingFlags.SYNC_CREATE |
            GObject.BindingFlags.BIDIRECTIONAL);

        this._title = new St.Label({
            style_class: 'quick-toggle-title',
            y_align: Clutter.ActorAlign.CENTER,
            x_align: Clutter.ActorAlign.START,
            x_expand: true,
        });
        this.label_actor = this._title;

        this._subtitle = new St.Label({
            style_class: 'quick-toggle-subtitle',
            y_align: Clutter.ActorAlign.CENTER,
            x_align: Clutter.ActorAlign.START,
            x_expand: true,
        });

        const titleBox = new St.BoxLayout({
            y_align: Clutter.ActorAlign.CENTER,
            x_align: Clutter.ActorAlign.START,
            x_expand: true,
            vertical: true,
        });
        titleBox.add_child(this._title);
        titleBox.add_child(this._subtitle);
        this._box.add_child(titleBox);

        this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END;

        this.bind_property('title',
            this._title, 'text',
            GObject.BindingFlags.SYNC_CREATE);

        this.bind_property('subtitle',
            this._subtitle, 'text',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property_full('subtitle',
            this._subtitle, 'visible',
            GObject.BindingFlags.SYNC_CREATE,
            (bind, source) => [true, source !== null],
            null);
    }

    get label() {
        console.warn('Trying to get label from QuickToggle. Use title instead.');
        return this.title;
    }

    set label(label) {
        console.warn('Trying to set label on QuickToggle. Use title instead.');
        this.title = label;
    }
});

export const QuickMenuToggle = GObject.registerClass({
    Properties: {
        'title': GObject.ParamSpec.string('title', '', '',
            GObject.ParamFlags.READWRITE,
            null),
        'subtitle': GObject.ParamSpec.string('subtitle', '', '',
            GObject.ParamFlags.READWRITE,
            null),
        'icon-name': GObject.ParamSpec.override('icon-name', St.Button),
        'gicon': GObject.ParamSpec.object('gicon', '', '',
            GObject.ParamFlags.READWRITE,
            Gio.Icon),
        'menu-enabled': GObject.ParamSpec.boolean(
            'menu-enabled', '', '',
            GObject.ParamFlags.READWRITE,
            true),
    },
}, class QuickMenuToggle extends QuickSettingsItem {
    _init(params) {
        super._init({
            ...params,
            hasMenu: true,
        });

        this.add_style_class_name('quick-menu-toggle');

        this._box = new St.BoxLayout({x_expand: true});
        this.set_child(this._box);

        const contents = new QuickToggle({
            x_expand: true,
        });
        this._box.add_child(contents);

        // Use an effect to lighten the menu button a bit, so we don't
        // have to define two full sets of button styles (normal/default)
        // with slightly different colors
        const menuHighlight = new Clutter.BrightnessContrastEffect();
        menuHighlight.set_brightness(MENU_BUTTON_BRIGHTNESS);

        this._menuButton = new St.Button({
            style_class: 'quick-toggle-arrow icon-button',
            child: new St.Icon({icon_name: 'go-next-symbolic'}),
            accessible_name: _('Open menu'),
            effect: menuHighlight,
            can_focus: true,
            x_expand: false,
            y_expand: true,
        });
        this._box.add_child(this._menuButton);

        this.bind_property('toggle-mode',
            contents, 'toggle-mode',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('checked',
            contents, 'checked',
            GObject.BindingFlags.SYNC_CREATE |
            GObject.BindingFlags.BIDIRECTIONAL);
        this.bind_property('title',
            contents, 'title',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('subtitle',
            contents, 'subtitle',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('gicon',
            contents, 'gicon',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('icon-name',
            contents, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE);

        this.bind_property('menu-enabled',
            this._menuButton, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('reactive',
            this._menuButton, 'reactive',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('checked',
            this._menuButton, 'checked',
            GObject.BindingFlags.SYNC_CREATE);
        contents.connect('clicked', (o, button) => this.emit('clicked', button));
        this._menuButton.connect('clicked', () => this.menu.open());
        this._menuButton.connect('popup-menu', () => this.emit('popup-menu'));
        contents.connect('popup-menu', () => this.emit('popup-menu'));
        this.connect('popup-menu', () => {
            if (this.menuEnabled)
                this.menu.open();
        });
    }
});

export const QuickSlider = GObject.registerClass({
    Properties: {
        'icon-name': GObject.ParamSpec.override('icon-name', St.Button),
        'gicon': GObject.ParamSpec.object('gicon', '', '',
            GObject.ParamFlags.READWRITE,
            Gio.Icon),
        'icon-reactive': GObject.ParamSpec.boolean(
            'icon-reactive', '', '',
            GObject.ParamFlags.READWRITE,
            false),
        'icon-label': GObject.ParamSpec.string(
            'icon-label', '', '',
            GObject.ParamFlags.READWRITE,
            ''),
        'menu-enabled': GObject.ParamSpec.boolean(
            'menu-enabled', '', '',
            GObject.ParamFlags.READWRITE,
            false),
    },
    Signals: {
        'icon-clicked': {},
    },
}, class QuickSlider extends QuickSettingsItem {
    _init(params) {
        super._init({
            style_class: 'quick-slider',
            ...params,
            can_focus: false,
            reactive: false,
            hasMenu: true,
        });

        const box = new St.BoxLayout({x_expand: true});
        this.set_child(box);

        const iconProps = {};
        if (this.gicon)
            iconProps['gicon'] = this.gicon;
        if (this.iconName)
            iconProps['icon-name'] = this.iconName;

        this._icon = new St.Icon({
            ...iconProps,
        });
        this._iconButton = new St.Button({
            child: this._icon,
            style_class: 'icon-button flat',
            can_focus: true,
            x_expand: false,
            y_expand: true,
        });
        this._iconButton.connect('clicked',
            () => this.emit('icon-clicked'));
        // Show as regular icon when non-interactive
        this._iconButton.connect('notify::reactive',
            () => this._iconButton.remove_style_pseudo_class('insensitive'));
        box.add_child(this._iconButton);

        this.bind_property('icon-reactive',
            this._iconButton, 'reactive',
            GObject.BindingFlags.SYNC_CREATE);
        this.bind_property('icon-label',
            this._iconButton, 'accessible-name',
            GObject.BindingFlags.SYNC_CREATE);

        // bindings are in the "wrong" direction, so we
        // pick up StIcon's linking of the two properties
        this._icon.bind_property('icon-name',
            this, 'icon-name',
            GObject.BindingFlags.SYNC_CREATE |
            GObject.BindingFlags.BIDIRECTIONAL);
        this._icon.bind_property('gicon',
            this, 'gicon',
            GObject.BindingFlags.SYNC_CREATE |
            GObject.BindingFlags.BIDIRECTIONAL);

        this.slider = new Slider(0);

        // for focus indication
        const sliderBin = new St.Bin({
            style_class: 'slider-bin',
            child: this.slider,
            reactive: true,
            can_focus: true,
            x_expand: true,
            y_align: Clutter.ActorAlign.CENTER,
        });
        box.add_child(sliderBin);

        // Make the slider bin transparent for a11y
        const sliderAccessible = this.slider.get_accessible();
        sliderAccessible.set_parent(sliderBin.get_parent().get_accessible());
        sliderBin.set_accessible(sliderAccessible);
        sliderBin.connect('event', (bin, event) => this.slider.event(event, false));

        this._menuButton = new St.Button({
            child: new St.Icon({icon_name: 'go-next-symbolic'}),
            style_class: 'icon-button flat',
            can_focus: true,
            x_expand: false,
            y_expand: true,
        });
        box.add_child(this._menuButton);

        this.bind_property('menu-enabled',
            this._menuButton, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
        this._menuButton.connect('clicked', () => this.menu.open());
        this.slider.connect('popup-menu', () => {
            if (this.menuEnabled)
                this.menu.open();
        });
    }
});

class QuickToggleMenu extends PopupMenu.PopupMenuBase {
    constructor(sourceActor) {
        super(sourceActor, 'quick-toggle-menu');

        const constraints = new Clutter.BindConstraint({
            coordinate: Clutter.BindCoordinate.Y,
            source: sourceActor,
        });
        sourceActor.bind_property('height',
            constraints, 'offset',
            GObject.BindingFlags.DEFAULT);

        this.actor = new St.Widget({
            layout_manager: new Clutter.BinLayout(),
            style_class: 'quick-toggle-menu-container',
            reactive: true,
            x_expand: true,
            y_expand: false,
            constraints,
        });
        this.actor._delegate = this;
        this.actor.add_child(this.box);

        global.focus_manager.add_group(this.actor);

        const headerLayout = new Clutter.GridLayout();
        this._header = new St.Widget({
            style_class: 'header',
            layout_manager: headerLayout,
            visible: false,
        });
        headerLayout.hookup_style(this._header);
        this.box.add_child(this._header);

        this._headerIcon = new St.Icon({
            style_class: 'icon',
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._headerTitle = new St.Label({
            style_class: 'title',
            y_align: Clutter.ActorAlign.CENTER,
            y_expand: true,
        });
        this._headerSubtitle = new St.Label({
            style_class: 'subtitle',
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._headerSpacer = new Clutter.Actor({x_expand: true});

        const side = this.actor.text_direction === Clutter.TextDirection.RTL
            ? Clutter.GridPosition.LEFT
            : Clutter.GridPosition.RIGHT;

        headerLayout.attach(this._headerIcon, 0, 0, 1, 2);
        headerLayout.attach_next_to(this._headerTitle,
            this._headerIcon, side, 1, 1);
        headerLayout.attach_next_to(this._headerSpacer,
            this._headerTitle, side, 1, 1);
        headerLayout.attach_next_to(this._headerSubtitle,
            this._headerTitle, Clutter.GridPosition.BOTTOM, 1, 1);

        sourceActor.connect('notify::checked',
            () => this._syncChecked());
        this._syncChecked();
    }

    setHeader(icon, title, subtitle = '') {
        if (icon instanceof Gio.Icon)
            this._headerIcon.gicon = icon;
        else
            this._headerIcon.icon_name = icon;

        this._headerTitle.text = title;
        this._headerSubtitle.set({
            text: subtitle,
            visible: !!subtitle,
        });

        this._header.show();
    }

    addHeaderSuffix(actor) {
        const {layoutManager: headerLayout} = this._header;
        const side = this.actor.text_direction === Clutter.TextDirection.RTL
            ? Clutter.GridPosition.LEFT
            : Clutter.GridPosition.RIGHT;
        this._header.remove_child(this._headerSpacer);
        headerLayout.attach_next_to(actor, this._headerTitle, side, 1, 1);
        headerLayout.attach_next_to(this._headerSpacer, actor, side, 1, 1);
    }

    open(animate) {
        if (this.isOpen)
            return;

        this.actor.show();
        this.isOpen = true;

        this.actor.height = -1;
        const [targetHeight] = this.actor.get_preferred_height(-1);

        const duration = animate !== PopupAnimation.NONE
            ? POPUP_ANIMATION_TIME / 2
            : 0;

        this.actor.height = 0;
        this.box.opacity = 0;
        this.actor.ease({
            duration,
            height: targetHeight,
            onComplete: () => {
                this.box.ease({
                    duration,
                    opacity: 255,
                });
                this.actor.height = -1;
            },
        });
        this.emit('open-state-changed', true);
    }

    close(animate) {
        if (!this.isOpen)
            return;

        const duration = animate !== PopupAnimation.NONE
            ? POPUP_ANIMATION_TIME / 2
            : 0;

        this.box.ease({
            duration,
            opacity: 0,
            onComplete: () => {
                this.actor.ease({
                    duration,
                    height: 0,
                    onComplete: () => {
                        this.actor.hide();
                        this.emit('menu-closed');
                    },
                });
            },
        });

        this.isOpen = false;
        this.emit('open-state-changed', false);
    }

    _syncChecked() {
        if (this.sourceActor.checked)
            this._headerIcon.add_style_class_name('active');
        else
            this._headerIcon.remove_style_class_name('active');
    }

    // expected on toplevel menus
    _setOpenedSubMenu(submenu) {
        this._openedSubMenu?.close(true);
        this._openedSubMenu = submenu;
    }
}

const QuickSettingsLayoutMeta = GObject.registerClass({
    Properties: {
        'column-span': GObject.ParamSpec.int(
            'column-span', '', '',
            GObject.ParamFlags.READWRITE,
            1, GLib.MAXINT32, 1),
    },
}, class QuickSettingsLayoutMeta extends Clutter.LayoutMeta {});

const QuickSettingsLayout = GObject.registerClass({
    Properties: {
        'row-spacing': GObject.ParamSpec.int(
            'row-spacing', 'row-spacing', 'row-spacing',
            GObject.ParamFlags.READWRITE,
            0, GLib.MAXINT32, 0),
        'column-spacing': GObject.ParamSpec.int(
            'column-spacing', 'column-spacing', 'column-spacing',
            GObject.ParamFlags.READWRITE,
            0, GLib.MAXINT32, 0),
        'n-columns': GObject.ParamSpec.int(
            'n-columns', 'n-columns', 'n-columns',
            GObject.ParamFlags.READWRITE,
            1, GLib.MAXINT32, 1),
    },
}, class QuickSettingsLayout extends Clutter.LayoutManager {
    _init(overlay, params) {
        super._init(params);

        this._overlay = overlay;
    }

    _containerStyleChanged() {
        const node = this._container.get_theme_node();

        let changed = false;
        let found, length;
        [found, length] = node.lookup_length('spacing-rows', false);
        changed ||= found;
        if (found)
            this.rowSpacing = length;

        [found, length] = node.lookup_length('spacing-columns', false);
        changed ||= found;
        if (found)
            this.columnSpacing = length;

        if (changed)
            this.layout_changed();
    }

    _getColSpan(container, child) {
        const {columnSpan} = this.get_child_meta(container, child);
        return Math.clamp(columnSpan, 1, this.nColumns);
    }

    _getMaxChildWidth(container) {
        let [minWidth, natWidth] = [0, 0];

        for (const child of container) {
            if (child === this._overlay)
                continue;

            const [childMin, childNat] = child.get_preferred_width(-1);
            const colSpan = this._getColSpan(container, child);
            minWidth = Math.max(minWidth, childMin / colSpan);
            natWidth = Math.max(natWidth, childNat / colSpan);
        }

        return [minWidth, natWidth];
    }

    _getRows(container) {
        const rows = [];
        let lineIndex = 0;
        let curRow;

        /** private */
        function appendRow() {
            curRow = [];
            rows.push(curRow);
            lineIndex = 0;
        }

        for (const child of container) {
            if (!child.visible)
                continue;

            if (child === this._overlay)
                continue;

            if (lineIndex === 0)
                appendRow();

            const colSpan = this._getColSpan(container, child);
            const fitsRow = lineIndex + colSpan <= this.nColumns;

            if (!fitsRow)
                appendRow();

            curRow.push(child);
            lineIndex = (lineIndex + colSpan) % this.nColumns;
        }

        return rows;
    }

    _getRowHeight(children) {
        let [minHeight, natHeight] = [0, 0];

        children.forEach(child => {
            const [childMin, childNat] = child.get_preferred_height(-1);
            minHeight = Math.max(minHeight, childMin);
            natHeight = Math.max(natHeight, childNat);
        });

        return [minHeight, natHeight];
    }

    vfunc_get_child_meta_type() {
        return QuickSettingsLayoutMeta.$gtype;
    }

    vfunc_set_container(container) {
        this._container?.disconnectObject(this);

        this._container = container;

        this._container?.connectObject('style-changed',
            () => this._containerStyleChanged(), this);
    }

    vfunc_get_preferred_width(container, _forHeight) {
        const [childMin, childNat] = this._getMaxChildWidth(container);
        const spacing = (this.nColumns - 1) * this.column_spacing;
        return [this.nColumns * childMin + spacing, this.nColumns * childNat + spacing];
    }

    vfunc_get_preferred_height(container, _forWidth) {
        const rows = this._getRows(container);

        let [minHeight, natHeight] = this._overlay.get_preferred_height(-1);

        const spacing = (rows.length - 1) * this.row_spacing;
        minHeight += spacing;
        natHeight += spacing;

        rows.forEach(row => {
            const [rowMin, rowNat] = this._getRowHeight(row);
            minHeight += rowMin;
            natHeight += rowNat;
        });

        return [minHeight, natHeight];
    }

    vfunc_allocate(container, box) {
        const rows = this._getRows(container);

        const [, overlayHeight] = this._overlay.get_preferred_height(-1);

        const availWidth = box.get_width() - (this.nColumns - 1) * this.column_spacing;
        const childWidth = Math.floor(availWidth / this.nColumns);

        this._overlay.allocate_available_size(0, 0, box.get_width(), box.get_height());

        const isRtl = container.text_direction === Clutter.TextDirection.RTL;

        const childBox = new Clutter.ActorBox();
        let y = box.y1;
        rows.forEach(row => {
            const [, rowNat] = this._getRowHeight(row);

            let lineIndex = 0;
            row.forEach(child => {
                const colSpan = this._getColSpan(container, child);
                const width =
                    childWidth * colSpan + this.column_spacing * (colSpan - 1);
                let x = box.x1 + lineIndex * (childWidth + this.column_spacing);
                if (isRtl)
                    x = box.x2 - width - x;

                childBox.set_origin(x, y);
                childBox.set_size(width, rowNat);
                child.allocate(childBox);

                lineIndex = (lineIndex + colSpan) % this.nColumns;
            });

            y += rowNat + this.row_spacing;

            if (row.some(c => c.menu?.actor.visible))
                y += overlayHeight;
        });
    }
});

export const QuickSettingsMenu = class extends PopupMenu.PopupMenu {
    constructor(sourceActor, nColumns = 1) {
        super(sourceActor, 0, St.Side.TOP);

        this.actor = new St.Widget({reactive: true, width: 0, height: 0});
        this.actor.add_child(this._boxPointer);
        this.actor._delegate = this;

        this.connect('menu-closed', () => this.actor.hide());

        Main.layoutManager.connectObject('system-modal-opened',
            () => this.close(), this);

        this._dimEffect = new Clutter.BrightnessContrastEffect({
            enabled: false,
        });
        this._boxPointer.add_effect_with_name('dim', this._dimEffect);
        this.box.add_style_class_name('quick-settings');

        // Overlay layer for menus
        this._overlay = new Clutter.Actor({
            layout_manager: new Clutter.BinLayout(),
        });

        // "clone"
        const placeholder = new Clutter.Actor({
            constraints: new Clutter.BindConstraint({
                coordinate: Clutter.BindCoordinate.HEIGHT,
                source: this._overlay,
            }),
        });

        this._grid = new St.Widget({
            style_class: 'quick-settings-grid',
            layout_manager: new QuickSettingsLayout(placeholder, {
                nColumns,
            }),
        });
        this.box.add_child(this._grid);
        this._grid.add_child(placeholder);

        const yConstraint = new Clutter.BindConstraint({
            coordinate: Clutter.BindCoordinate.Y,
            source: this._boxPointer,
        });

        // Pick up additional spacing from any intermediate actors
        const updateOffset = () => {
            const laters = global.compositor.get_laters();
            laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                const offset = this._grid.apply_relative_transform_to_point(
                    this._boxPointer, new Graphene.Point3D());
                yConstraint.offset = offset.y;
                return GLib.SOURCE_REMOVE;
            });
        };
        this._grid.connect('notify::y', updateOffset);
        this.box.connect('notify::y', updateOffset);
        this._boxPointer.bin.connect('notify::y', updateOffset);

        this._overlay.add_constraint(yConstraint);
        this._overlay.add_constraint(new Clutter.BindConstraint({
            coordinate: Clutter.BindCoordinate.X,
            source: this._boxPointer,
        }));
        this._overlay.add_constraint(new Clutter.BindConstraint({
            coordinate: Clutter.BindCoordinate.WIDTH,
            source: this._boxPointer,
        }));

        this.actor.add_child(this._overlay);
    }

    addItem(item, colSpan = 1) {
        this._grid.add_child(item);
        this._completeAddItem(item, colSpan);
    }

    insertItemBefore(item, sibling, colSpan = 1) {
        this._grid.insert_child_below(item, sibling);
        this._completeAddItem(item, colSpan);
    }

    _completeAddItem(item, colSpan) {
        this._grid.layout_manager.child_set_property(
            this._grid, item, 'column-span', colSpan);

        if (item.menu) {
            this._overlay.add_child(item.menu.actor);

            item.menu.connect('open-state-changed', (m, isOpen) => {
                this._setDimmed(isOpen);
                this._activeMenu = isOpen ? item.menu : null;
            });
        }
    }

    getFirstItem() {
        return this._grid.get_first_child();
    }

    open(animate) {
        this.actor.show();
        super.open(animate);
    }

    close(animate) {
        this._activeMenu?.close(animate);
        super.close(animate);
    }

    _setDimmed(dim) {
        const val = 127 * (1 + (dim ? 1 : 0) * DIM_BRIGHTNESS);
        const color = Clutter.Color.new(val, val, val, 255);

        this._boxPointer.ease_property('@effects.dim.brightness', color, {
            mode: Clutter.AnimationMode.LINEAR,
            duration: POPUP_ANIMATION_TIME,
            onStopped: () => (this._dimEffect.enabled = dim),
        });
        this._dimEffect.enabled = true;
    }
};

export const SystemIndicator = GObject.registerClass(
class SystemIndicator extends St.BoxLayout {
    _init() {
        super._init({
            style_class: 'panel-status-indicators-box',
            reactive: true,
            visible: false,
        });

        this.quickSettingsItems = [];
    }

    _syncIndicatorsVisible() {
        this.visible = this.get_children().some(a => a.visible);
    }

    _addIndicator() {
        const icon = new St.Icon({style_class: 'system-status-icon'});
        this.add_child(icon);
        icon.connect('notify::visible', () => this._syncIndicatorsVisible());
        this._syncIndicatorsVisible();
        return icon;
    }
});
(uuay)animationUtils.jsP
import St from 'gi://St';
import Clutter from 'gi://Clutter';

import * as Params from './params.js';

const SCROLL_TIME = 100;

const WIGGLE_OFFSET = 6;
const WIGGLE_DURATION = 65;
const N_WIGGLES = 3;

/**
 * adjustAnimationTime:
 *
 * @param {number} msecs - time in milliseconds
 *
 * Adjust `msecs` to account for St's enable-animations
 * and slow-down-factor settings
 */
export function adjustAnimationTime(msecs) {
    const settings = St.Settings.get();

    if (!settings.enable_animations)
        return 0;
    return settings.slow_down_factor * msecs;
}

/**
 * Animate scrolling a scrollview until an actor is visible.
 *
 * @param {St.ScrollView} scrollView - the scroll view the actor is in
 * @param {Clutter.Actor} actor - the actor
 */
export function ensureActorVisibleInScrollView(scrollView, actor) {
    const adjustment = scrollView.vadjustment;
    let [value, lower_, upper, stepIncrement_, pageIncrement_, pageSize] = adjustment.get_values();

    let offset = 0;
    const vfade = scrollView.get_effect('fade');
    if (vfade)
        offset = vfade.fade_margins.top;

    let box = actor.get_allocation_box();
    let y1 = box.y1, y2 = box.y2;

    let parent = actor.get_parent();
    while (parent !== scrollView) {
        if (!parent)
            throw new Error('actor not in scroll view');

        box = parent.get_allocation_box();
        y1 += box.y1;
        y2 += box.y1;
        parent = parent.get_parent();
    }

    if (y1 < value + offset)
        value = Math.max(0, y1 - offset);
    else if (y2 > value + pageSize - offset)
        value = Math.min(upper, y2 + offset - pageSize);
    else
        return;

    adjustment.ease(value, {
        mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        duration: SCROLL_TIME,
    });
}

/**
 * "Wiggles" a clutter actor. A "wiggle" is an animation the moves an actor
 * back and forth on the X axis a specified amount of times.
 *
 * @param {Clutter.Actor} actor - an actor to animate
 * @param {object} params - options for the animation
 * @param {number} params.offset - the offset to move the actor by per-wiggle
 * @param {number} params.duration - the amount of time to move the actor per-wiggle
 * @param {number} params.wiggleCount - the number of times to wiggle the actor
 */
export function wiggle(actor, params) {
    if (!St.Settings.get().enable_animations)
        return;

    params = Params.parse(params, {
        offset: WIGGLE_OFFSET,
        duration: WIGGLE_DURATION,
        wiggleCount: N_WIGGLES,
    });
    actor.translation_x = 0;

    // Accelerate before wiggling
    actor.ease({
        translation_x: -params.offset,
        duration: params.duration,
        mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        onComplete: () => {
            // Wiggle
            actor.ease({
                translation_x: params.offset,
                duration: params.duration,
                mode: Clutter.AnimationMode.LINEAR,
                repeatCount: params.wiggleCount,
                autoReverse: true,
                onComplete: () => {
                    // Decelerate and return to the original position
                    actor.ease({
                        translation_x: 0,
                        duration: params.duration,
                        mode: Clutter.AnimationMode.EASE_IN_QUAD,
                    });
                },
            });
        },
    });
}
(uuay)authdProtocol.min.jst6var commonjsGlobal="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},indexMinimal={},minimal$1={},aspromise,hasRequiredAspromise;function requireAspromise(){if(hasRequiredAspromise)return aspromise;return hasRequiredAspromise=1,aspromise=function(t,e){var o=new Array(arguments.length-1),r=0,n=2,i=!0;for(;n<arguments.length;)o[r++]=arguments[n++];return new Promise((function(n,s){o[r]=function(t){if(i)if(i=!1,t)s(t);else{for(var e=new Array(arguments.length-1),o=0;o<e.length;)e[o++]=arguments[o];n.apply(null,e)}};try{t.apply(e||null,o)}catch(t){i&&(i=!1,s(t))}}))},aspromise}var base64$1={},hasRequiredBase64,eventemitter,hasRequiredEventemitter,float,hasRequiredFloat,inquire_1,hasRequiredInquire;function requireBase64(){return hasRequiredBase64||(hasRequiredBase64=1,function(t){var e=base64$1;e.length=function(t){var e=t.length;if(!e)return 0;for(var o=0;--e%4>1&&"="===t.charAt(e);)++o;return Math.ceil(3*t.length)/4-o};for(var o=new Array(64),r=new Array(123),n=0;n<64;)r[o[n]=n<26?n+65:n<52?n+71:n<62?n-4:n-59|43]=n++;e.encode=function(t,e,r){for(var n,i=null,s=[],u=0,a=0;e<r;){var l=t[e++];switch(a){case 0:s[u++]=o[l>>2],n=(3&l)<<4,a=1;break;case 1:s[u++]=o[n|l>>4],n=(15&l)<<2,a=2;break;case 2:s[u++]=o[n|l>>6],s[u++]=o[63&l],a=0}u>8191&&((i||(i=[])).push(String.fromCharCode.apply(String,s)),u=0)}return a&&(s[u++]=o[n],s[u++]=61,1===a&&(s[u++]=61)),i?(u&&i.push(String.fromCharCode.apply(String,s.slice(0,u))),i.join("")):String.fromCharCode.apply(String,s.slice(0,u))};var i="invalid encoding";e.decode=function(t,e,o){for(var n,s=o,u=0,a=0;a<t.length;){var l=t.charCodeAt(a++);if(61===l&&u>1)break;if(void 0===(l=r[l]))throw Error(i);switch(u){case 0:n=l,u=1;break;case 1:e[o++]=n<<2|(48&l)>>4,n=l,u=2;break;case 2:e[o++]=(15&n)<<4|(60&l)>>2,n=l,u=3;break;case 3:e[o++]=(3&n)<<6|l,u=0}}if(1===u)throw Error(i);return o-s},e.test=function(t){return/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(t)}}()),base64$1}function requireEventemitter(){if(hasRequiredEventemitter)return eventemitter;function t(){this._listeners={}}return hasRequiredEventemitter=1,eventemitter=t,t.prototype.on=function(t,e,o){return(this._listeners[t]||(this._listeners[t]=[])).push({fn:e,ctx:o||this}),this},t.prototype.off=function(t,e){if(void 0===t)this._listeners={};else if(void 0===e)this._listeners[t]=[];else for(var o=this._listeners[t],r=0;r<o.length;)o[r].fn===e?o.splice(r,1):++r;return this},t.prototype.emit=function(t){var e=this._listeners[t];if(e){for(var o=[],r=1;r<arguments.length;)o.push(arguments[r++]);for(r=0;r<e.length;)e[r].fn.apply(e[r++].ctx,o)}return this},eventemitter}function requireFloat(){if(hasRequiredFloat)return float;function t(t){return"undefined"!=typeof Float32Array?function(){var e=new Float32Array([-0]),o=new Uint8Array(e.buffer),r=128===o[3];function n(t,r,n){e[0]=t,r[n]=o[0],r[n+1]=o[1],r[n+2]=o[2],r[n+3]=o[3]}function i(t,r,n){e[0]=t,r[n]=o[3],r[n+1]=o[2],r[n+2]=o[1],r[n+3]=o[0]}function s(t,r){return o[0]=t[r],o[1]=t[r+1],o[2]=t[r+2],o[3]=t[r+3],e[0]}function u(t,r){return o[3]=t[r],o[2]=t[r+1],o[1]=t[r+2],o[0]=t[r+3],e[0]}t.writeFloatLE=r?n:i,t.writeFloatBE=r?i:n,t.readFloatLE=r?s:u,t.readFloatBE=r?u:s}():function(){function i(t,e,o,r){var n=e<0?1:0;if(n&&(e=-e),0===e)t(1/e>0?0:2147483648,o,r);else if(isNaN(e))t(2143289344,o,r);else if(e>34028234663852886e22)t((n<<31|2139095040)>>>0,o,r);else if(e<11754943508222875e-54)t((n<<31|Math.round(e/1401298464324817e-60))>>>0,o,r);else{var i=Math.floor(Math.log(e)/Math.LN2);t((n<<31|i+127<<23|8388607&Math.round(e*Math.pow(2,-i)*8388608))>>>0,o,r)}}function s(t,e,o){var r=t(e,o),n=2*(r>>31)+1,i=r>>>23&255,s=8388607&r;return 255===i?s?NaN:n*(1/0):0===i?1401298464324817e-60*n*s:n*Math.pow(2,i-150)*(s+8388608)}t.writeFloatLE=i.bind(null,e),t.writeFloatBE=i.bind(null,o),t.readFloatLE=s.bind(null,r),t.readFloatBE=s.bind(null,n)}(),"undefined"!=typeof Float64Array?function(){var e=new Float64Array([-0]),o=new Uint8Array(e.buffer),r=128===o[7];function n(t,r,n){e[0]=t,r[n]=o[0],r[n+1]=o[1],r[n+2]=o[2],r[n+3]=o[3],r[n+4]=o[4],r[n+5]=o[5],r[n+6]=o[6],r[n+7]=o[7]}function i(t,r,n){e[0]=t,r[n]=o[7],r[n+1]=o[6],r[n+2]=o[5],r[n+3]=o[4],r[n+4]=o[3],r[n+5]=o[2],r[n+6]=o[1],r[n+7]=o[0]}function s(t,r){return o[0]=t[r],o[1]=t[r+1],o[2]=t[r+2],o[3]=t[r+3],o[4]=t[r+4],o[5]=t[r+5],o[6]=t[r+6],o[7]=t[r+7],e[0]}function u(t,r){return o[7]=t[r],o[6]=t[r+1],o[5]=t[r+2],o[4]=t[r+3],o[3]=t[r+4],o[2]=t[r+5],o[1]=t[r+6],o[0]=t[r+7],e[0]}t.writeDoubleLE=r?n:i,t.writeDoubleBE=r?i:n,t.readDoubleLE=r?s:u,t.readDoubleBE=r?u:s}():function(){function i(t,e,o,r,n,i){var s=r<0?1:0;if(s&&(r=-r),0===r)t(0,n,i+e),t(1/r>0?0:2147483648,n,i+o);else if(isNaN(r))t(0,n,i+e),t(2146959360,n,i+o);else if(r>17976931348623157e292)t(0,n,i+e),t((s<<31|2146435072)>>>0,n,i+o);else{var u;if(r<22250738585072014e-324)t((u=r/5e-324)>>>0,n,i+e),t((s<<31|u/4294967296)>>>0,n,i+o);else{var a=Math.floor(Math.log(r)/Math.LN2);1024===a&&(a=1023),t(4503599627370496*(u=r*Math.pow(2,-a))>>>0,n,i+e),t((s<<31|a+1023<<20|1048576*u&1048575)>>>0,n,i+o)}}}function s(t,e,o,r,n){var i=t(r,n+e),s=t(r,n+o),u=2*(s>>31)+1,a=s>>>20&2047,l=4294967296*(1048575&s)+i;return 2047===a?l?NaN:u*(1/0):0===a?5e-324*u*l:u*Math.pow(2,a-1075)*(l+4503599627370496)}t.writeDoubleLE=i.bind(null,e,0,4),t.writeDoubleBE=i.bind(null,o,4,0),t.readDoubleLE=s.bind(null,r,0,4),t.readDoubleBE=s.bind(null,n,4,0)}(),t}function e(t,e,o){e[o]=255&t,e[o+1]=t>>>8&255,e[o+2]=t>>>16&255,e[o+3]=t>>>24}function o(t,e,o){e[o]=t>>>24,e[o+1]=t>>>16&255,e[o+2]=t>>>8&255,e[o+3]=255&t}function r(t,e){return(t[e]|t[e+1]<<8|t[e+2]<<16|t[e+3]<<24)>>>0}function n(t,e){return(t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3])>>>0}return hasRequiredFloat=1,float=t(t)}function requireInquire(){if(hasRequiredInquire)return inquire_1;function inquire(moduleName){try{var mod=eval("quire".replace(/^/,"re"))(moduleName);if(mod&&(mod.length||Object.keys(mod).length))return mod}catch(t){}return null}return hasRequiredInquire=1,inquire_1=inquire,inquire_1}var utf8$2={},hasRequiredUtf8,pool_1,hasRequiredPool,longbits,hasRequiredLongbits,hasRequiredMinimal;function requireUtf8(){return hasRequiredUtf8||(hasRequiredUtf8=1,function(t){var e=utf8$2;e.length=function(t){for(var e=0,o=0,r=0;r<t.length;++r)(o=t.charCodeAt(r))<128?e+=1:o<2048?e+=2:55296==(64512&o)&&56320==(64512&t.charCodeAt(r+1))?(++r,e+=4):e+=3;return e},e.read=function(t,e,o){if(o-e<1)return"";for(var r,n=null,i=[],s=0;e<o;)(r=t[e++])<128?i[s++]=r:r>191&&r<224?i[s++]=(31&r)<<6|63&t[e++]:r>239&&r<365?(r=((7&r)<<18|(63&t[e++])<<12|(63&t[e++])<<6|63&t[e++])-65536,i[s++]=55296+(r>>10),i[s++]=56320+(1023&r)):i[s++]=(15&r)<<12|(63&t[e++])<<6|63&t[e++],s>8191&&((n||(n=[])).push(String.fromCharCode.apply(String,i)),s=0);return n?(s&&n.push(String.fromCharCode.apply(String,i.slice(0,s))),n.join("")):String.fromCharCode.apply(String,i.slice(0,s))},e.write=function(t,e,o){for(var r,n,i=o,s=0;s<t.length;++s)(r=t.charCodeAt(s))<128?e[o++]=r:r<2048?(e[o++]=r>>6|192,e[o++]=63&r|128):55296==(64512&r)&&56320==(64512&(n=t.charCodeAt(s+1)))?(r=65536+((1023&r)<<10)+(1023&n),++s,e[o++]=r>>18|240,e[o++]=r>>12&63|128,e[o++]=r>>6&63|128,e[o++]=63&r|128):(e[o++]=r>>12|224,e[o++]=r>>6&63|128,e[o++]=63&r|128);return o-i}}()),utf8$2}function requirePool(){if(hasRequiredPool)return pool_1;return hasRequiredPool=1,pool_1=function(t,e,o){var r=o||8192,n=r>>>1,i=null,s=r;return function(o){if(o<1||o>n)return t(o);s+o>r&&(i=t(r),s=0);var u=e.call(i,s,s+=o);return 7&s&&(s=1+(7|s)),u}}}function requireLongbits(){if(hasRequiredLongbits)return longbits;hasRequiredLongbits=1,longbits=e;var t=requireMinimal();function e(t,e){this.lo=t>>>0,this.hi=e>>>0}var o=e.zero=new e(0,0);o.toNumber=function(){return 0},o.zzEncode=o.zzDecode=function(){return this},o.length=function(){return 1};var r=e.zeroHash="\0\0\0\0\0\0\0\0";e.fromNumber=function(t){if(0===t)return o;var r=t<0;r&&(t=-t);var n=t>>>0,i=(t-n)/4294967296>>>0;return r&&(i=~i>>>0,n=~n>>>0,++n>4294967295&&(n=0,++i>4294967295&&(i=0))),new e(n,i)},e.from=function(r){if("number"==typeof r)return e.fromNumber(r);if(t.isString(r)){if(!t.Long)return e.fromNumber(parseInt(r,10));r=t.Long.fromString(r)}return r.low||r.high?new e(r.low>>>0,r.high>>>0):o},e.prototype.toNumber=function(t){if(!t&&this.hi>>>31){var e=1+~this.lo>>>0,o=~this.hi>>>0;return e||(o=o+1>>>0),-(e+4294967296*o)}return this.lo+4294967296*this.hi},e.prototype.toLong=function(e){return t.Long?new t.Long(0|this.lo,0|this.hi,Boolean(e)):{low:0|this.lo,high:0|this.hi,unsigned:Boolean(e)}};var n=String.prototype.charCodeAt;return e.fromHash=function(t){return t===r?o:new e((n.call(t,0)|n.call(t,1)<<8|n.call(t,2)<<16|n.call(t,3)<<24)>>>0,(n.call(t,4)|n.call(t,5)<<8|n.call(t,6)<<16|n.call(t,7)<<24)>>>0)},e.prototype.toHash=function(){return String.fromCharCode(255&this.lo,this.lo>>>8&255,this.lo>>>16&255,this.lo>>>24,255&this.hi,this.hi>>>8&255,this.hi>>>16&255,this.hi>>>24)},e.prototype.zzEncode=function(){var t=this.hi>>31;return this.hi=((this.hi<<1|this.lo>>>31)^t)>>>0,this.lo=(this.lo<<1^t)>>>0,this},e.prototype.zzDecode=function(){var t=-(1&this.lo);return this.lo=((this.lo>>>1|this.hi<<31)^t)>>>0,this.hi=(this.hi>>>1^t)>>>0,this},e.prototype.length=function(){var t=this.lo,e=(this.lo>>>28|this.hi<<4)>>>0,o=this.hi>>>24;return 0===o?0===e?t<16384?t<128?1:2:t<2097152?3:4:e<16384?e<128?5:6:e<2097152?7:8:o<128?9:10},longbits}function requireMinimal(){return hasRequiredMinimal||(hasRequiredMinimal=1,function(t){var e=minimal$1;function o(t,e,o){for(var r=Object.keys(e),n=0;n<r.length;++n)void 0!==t[r[n]]&&o||(t[r[n]]=e[r[n]]);return t}function r(t){function e(t,r){if(!(this instanceof e))return new e(t,r);Object.defineProperty(this,"message",{get:function(){return t}}),Error.captureStackTrace?Error.captureStackTrace(this,e):Object.defineProperty(this,"stack",{value:(new Error).stack||""}),r&&o(this,r)}return e.prototype=Object.create(Error.prototype,{constructor:{value:e,writable:!0,enumerable:!1,configurable:!0},name:{get:function(){return t},set:void 0,enumerable:!1,configurable:!0},toString:{value:function(){return this.name+": "+this.message},writable:!0,enumerable:!1,configurable:!0}}),e}e.asPromise=requireAspromise(),e.base64=requireBase64(),e.EventEmitter=requireEventemitter(),e.float=requireFloat(),e.inquire=requireInquire(),e.utf8=requireUtf8(),e.pool=requirePool(),e.LongBits=requireLongbits(),e.isNode=Boolean(void 0!==commonjsGlobal&&commonjsGlobal&&commonjsGlobal.process&&commonjsGlobal.process.versions&&commonjsGlobal.process.versions.node),e.global=e.isNode&&commonjsGlobal||"undefined"!=typeof window&&window||"undefined"!=typeof self&&self||commonjsGlobal,e.emptyArray=Object.freeze?Object.freeze([]):[],e.emptyObject=Object.freeze?Object.freeze({}):{},e.isInteger=Number.isInteger||function(t){return"number"==typeof t&&isFinite(t)&&Math.floor(t)===t},e.isString=function(t){return"string"==typeof t||t instanceof String},e.isObject=function(t){return t&&"object"==typeof t},e.isset=e.isSet=function(t,e){var o=t[e];return!(null==o||!t.hasOwnProperty(e))&&("object"!=typeof o||(Array.isArray(o)?o.length:Object.keys(o).length)>0)},e.Buffer=function(){try{var t=e.inquire("buffer").Buffer;return t.prototype.utf8Write?t:null}catch(t){return null}}(),e._Buffer_from=null,e._Buffer_allocUnsafe=null,e.newBuffer=function(t){return"number"==typeof t?e.Buffer?e._Buffer_allocUnsafe(t):new e.Array(t):e.Buffer?e._Buffer_from(t):"undefined"==typeof Uint8Array?t:new Uint8Array(t)},e.Array="undefined"!=typeof Uint8Array?Uint8Array:Array,e.Long=e.global.dcodeIO&&e.global.dcodeIO.Long||e.global.Long||e.inquire("long"),e.key2Re=/^true|false|0|1$/,e.key32Re=/^-?(?:0|[1-9][0-9]*)$/,e.key64Re=/^(?:[\\x00-\\xff]{8}|-?(?:0|[1-9][0-9]*))$/,e.longToHash=function(t){return t?e.LongBits.from(t).toHash():e.LongBits.zeroHash},e.longFromHash=function(t,o){var r=e.LongBits.fromHash(t);return e.Long?e.Long.fromBits(r.lo,r.hi,o):r.toNumber(Boolean(o))},e.merge=o,e.lcFirst=function(t){return t.charAt(0).toLowerCase()+t.substring(1)},e.newError=r,e.ProtocolError=r("ProtocolError"),e.oneOfGetter=function(t){for(var e={},o=0;o<t.length;++o)e[t[o]]=1;return function(){for(var t=Object.keys(this),o=t.length-1;o>-1;--o)if(1===e[t[o]]&&void 0!==this[t[o]]&&null!==this[t[o]])return t[o]}},e.oneOfSetter=function(t){return function(e){for(var o=0;o<t.length;++o)t[o]!==e&&delete this[t[o]]}},e.toJSONOptions={longs:String,enums:String,bytes:String,json:!0},e._configure=function(){var t=e.Buffer;t?(e._Buffer_from=t.from!==Uint8Array.from&&t.from||function(e,o){return new t(e,o)},e._Buffer_allocUnsafe=t.allocUnsafe||function(e){return new t(e)}):e._Buffer_from=e._Buffer_allocUnsafe=null}}()),minimal$1}var writer=Writer$1,util$4=requireMinimal(),BufferWriter$1,LongBits$1=util$4.LongBits,base64=util$4.base64,utf8$1=util$4.utf8;function Op(t,e,o){this.fn=t,this.len=e,this.next=void 0,this.val=o}function noop(){}function State(t){this.head=t.head,this.tail=t.tail,this.len=t.len,this.next=t.states}function Writer$1(){this.len=0,this.head=new Op(noop,0,0),this.tail=this.head,this.states=null}var create$1=function(){return util$4.Buffer?function(){return(Writer$1.create=function(){return new BufferWriter$1})()}:function(){return new Writer$1}};function writeByte(t,e,o){e[o]=255&t}function writeVarint32(t,e,o){for(;t>127;)e[o++]=127&t|128,t>>>=7;e[o]=t}function VarintOp(t,e){this.len=t,this.next=void 0,this.val=e}function writeVarint64(t,e,o){for(;t.hi;)e[o++]=127&t.lo|128,t.lo=(t.lo>>>7|t.hi<<25)>>>0,t.hi>>>=7;for(;t.lo>127;)e[o++]=127&t.lo|128,t.lo=t.lo>>>7;e[o++]=t.lo}function writeFixed32(t,e,o){e[o]=255&t,e[o+1]=t>>>8&255,e[o+2]=t>>>16&255,e[o+3]=t>>>24}Writer$1.create=create$1(),Writer$1.alloc=function(t){return new util$4.Array(t)},util$4.Array!==Array&&(Writer$1.alloc=util$4.pool(Writer$1.alloc,util$4.Array.prototype.subarray)),Writer$1.prototype._push=function(t,e,o){return this.tail=this.tail.next=new Op(t,e,o),this.len+=e,this},VarintOp.prototype=Object.create(Op.prototype),VarintOp.prototype.fn=writeVarint32,Writer$1.prototype.uint32=function(t){return this.len+=(this.tail=this.tail.next=new VarintOp((t>>>=0)<128?1:t<16384?2:t<2097152?3:t<268435456?4:5,t)).len,this},Writer$1.prototype.int32=function(t){return t<0?this._push(writeVarint64,10,LongBits$1.fromNumber(t)):this.uint32(t)},Writer$1.prototype.sint32=function(t){return this.uint32((t<<1^t>>31)>>>0)},Writer$1.prototype.uint64=function(t){var e=LongBits$1.from(t);return this._push(writeVarint64,e.length(),e)},Writer$1.prototype.int64=Writer$1.prototype.uint64,Writer$1.prototype.sint64=function(t){var e=LongBits$1.from(t).zzEncode();return this._push(writeVarint64,e.length(),e)},Writer$1.prototype.bool=function(t){return this._push(writeByte,1,t?1:0)},Writer$1.prototype.fixed32=function(t){return this._push(writeFixed32,4,t>>>0)},Writer$1.prototype.sfixed32=Writer$1.prototype.fixed32,Writer$1.prototype.fixed64=function(t){var e=LongBits$1.from(t);return this._push(writeFixed32,4,e.lo)._push(writeFixed32,4,e.hi)},Writer$1.prototype.sfixed64=Writer$1.prototype.fixed64,Writer$1.prototype.float=function(t){return this._push(util$4.float.writeFloatLE,4,t)},Writer$1.prototype.double=function(t){return this._push(util$4.float.writeDoubleLE,8,t)};var writeBytes=util$4.Array.prototype.set?function(t,e,o){e.set(t,o)}:function(t,e,o){for(var r=0;r<t.length;++r)e[o+r]=t[r]};Writer$1.prototype.bytes=function(t){var e=t.length>>>0;if(!e)return this._push(writeByte,1,0);if(util$4.isString(t)){var o=Writer$1.alloc(e=base64.length(t));base64.decode(t,o,0),t=o}return this.uint32(e)._push(writeBytes,e,t)},Writer$1.prototype.string=function(t){var e=utf8$1.length(t);return e?this.uint32(e)._push(utf8$1.write,e,t):this._push(writeByte,1,0)},Writer$1.prototype.fork=function(){return this.states=new State(this),this.head=this.tail=new Op(noop,0,0),this.len=0,this},Writer$1.prototype.reset=function(){return this.states?(this.head=this.states.head,this.tail=this.states.tail,this.len=this.states.len,this.states=this.states.next):(this.head=this.tail=new Op(noop,0,0),this.len=0),this},Writer$1.prototype.ldelim=function(){var t=this.head,e=this.tail,o=this.len;return this.reset().uint32(o),o&&(this.tail.next=t.next,this.tail=e,this.len+=o),this},Writer$1.prototype.finish=function(){for(var t=this.head.next,e=this.constructor.alloc(this.len),o=0;t;)t.fn(t.val,e,o),o+=t.len,t=t.next;return e},Writer$1._configure=function(t){BufferWriter$1=t,Writer$1.create=create$1(),BufferWriter$1._configure()};var writer_buffer=BufferWriter,Writer=writer;(BufferWriter.prototype=Object.create(Writer.prototype)).constructor=BufferWriter;var util$3=requireMinimal();function BufferWriter(){Writer.call(this)}function writeStringBuffer(t,e,o){t.length<40?util$3.utf8.write(t,e,o):e.utf8Write?e.utf8Write(t,o):e.write(t,o)}BufferWriter._configure=function(){BufferWriter.alloc=util$3._Buffer_allocUnsafe,BufferWriter.writeBytesBuffer=util$3.Buffer&&util$3.Buffer.prototype instanceof Uint8Array&&"set"===util$3.Buffer.prototype.set.name?function(t,e,o){e.set(t,o)}:function(t,e,o){if(t.copy)t.copy(e,o,0,t.length);else for(var r=0;r<t.length;)e[o++]=t[r++]}},BufferWriter.prototype.bytes=function(t){util$3.isString(t)&&(t=util$3._Buffer_from(t,"base64"));var e=t.length>>>0;return this.uint32(e),e&&this._push(BufferWriter.writeBytesBuffer,e,t),this},BufferWriter.prototype.string=function(t){var e=util$3.Buffer.byteLength(t);return this.uint32(e),e&&this._push(writeStringBuffer,e,t),this},BufferWriter._configure();var reader=Reader$1,util$2=requireMinimal(),BufferReader$1,LongBits=util$2.LongBits,utf8=util$2.utf8;function indexOutOfRange(t,e){return RangeError("index out of range: "+t.pos+" + "+(e||1)+" > "+t.len)}function Reader$1(t){this.buf=t,this.pos=0,this.len=t.length}var create_array="undefined"!=typeof Uint8Array?function(t){if(t instanceof Uint8Array||Array.isArray(t))return new Reader$1(t);throw Error("illegal buffer")}:function(t){if(Array.isArray(t))return new Reader$1(t);throw Error("illegal buffer")},create=function(){return util$2.Buffer?function(t){return(Reader$1.create=function(t){return util$2.Buffer.isBuffer(t)?new BufferReader$1(t):create_array(t)})(t)}:create_array};function readLongVarint(){var t=new LongBits(0,0),e=0;if(!(this.len-this.pos>4)){for(;e<3;++e){if(this.pos>=this.len)throw indexOutOfRange(this);if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*e)>>>0,this.buf[this.pos++]<128)return t}return t.lo=(t.lo|(127&this.buf[this.pos++])<<7*e)>>>0,t}for(;e<4;++e)if(t.lo=(t.lo|(127&this.buf[this.pos])<<7*e)>>>0,this.buf[this.pos++]<128)return t;if(t.lo=(t.lo|(127&this.buf[this.pos])<<28)>>>0,t.hi=(t.hi|(127&this.buf[this.pos])>>4)>>>0,this.buf[this.pos++]<128)return t;if(e=0,this.len-this.pos>4){for(;e<5;++e)if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*e+3)>>>0,this.buf[this.pos++]<128)return t}else for(;e<5;++e){if(this.pos>=this.len)throw indexOutOfRange(this);if(t.hi=(t.hi|(127&this.buf[this.pos])<<7*e+3)>>>0,this.buf[this.pos++]<128)return t}throw Error("invalid varint encoding")}function readFixed32_end(t,e){return(t[e-4]|t[e-3]<<8|t[e-2]<<16|t[e-1]<<24)>>>0}function readFixed64(){if(this.pos+8>this.len)throw indexOutOfRange(this,8);return new LongBits(readFixed32_end(this.buf,this.pos+=4),readFixed32_end(this.buf,this.pos+=4))}Reader$1.create=create(),Reader$1.prototype._slice=util$2.Array.prototype.subarray||util$2.Array.prototype.slice,Reader$1.prototype.uint32=function(){var t=4294967295;return function(){if(t=(127&this.buf[this.pos])>>>0,this.buf[this.pos++]<128)return t;if(t=(t|(127&this.buf[this.pos])<<7)>>>0,this.buf[this.pos++]<128)return t;if(t=(t|(127&this.buf[this.pos])<<14)>>>0,this.buf[this.pos++]<128)return t;if(t=(t|(127&this.buf[this.pos])<<21)>>>0,this.buf[this.pos++]<128)return t;if(t=(t|(15&this.buf[this.pos])<<28)>>>0,this.buf[this.pos++]<128)return t;if((this.pos+=5)>this.len)throw this.pos=this.len,indexOutOfRange(this,10);return t}}(),Reader$1.prototype.int32=function(){return 0|this.uint32()},Reader$1.prototype.sint32=function(){var t=this.uint32();return t>>>1^-(1&t)|0},Reader$1.prototype.bool=function(){return 0!==this.uint32()},Reader$1.prototype.fixed32=function(){if(this.pos+4>this.len)throw indexOutOfRange(this,4);return readFixed32_end(this.buf,this.pos+=4)},Reader$1.prototype.sfixed32=function(){if(this.pos+4>this.len)throw indexOutOfRange(this,4);return 0|readFixed32_end(this.buf,this.pos+=4)},Reader$1.prototype.float=function(){if(this.pos+4>this.len)throw indexOutOfRange(this,4);var t=util$2.float.readFloatLE(this.buf,this.pos);return this.pos+=4,t},Reader$1.prototype.double=function(){if(this.pos+8>this.len)throw indexOutOfRange(this,4);var t=util$2.float.readDoubleLE(this.buf,this.pos);return this.pos+=8,t},Reader$1.prototype.bytes=function(){var t=this.uint32(),e=this.pos,o=this.pos+t;if(o>this.len)throw indexOutOfRange(this,t);if(this.pos+=t,Array.isArray(this.buf))return this.buf.slice(e,o);if(e===o){var r=util$2.Buffer;return r?r.alloc(0):new this.buf.constructor(0)}return this._slice.call(this.buf,e,o)},Reader$1.prototype.string=function(){var t=this.bytes();return utf8.read(t,0,t.length)},Reader$1.prototype.skip=function(t){if("number"==typeof t){if(this.pos+t>this.len)throw indexOutOfRange(this,t);this.pos+=t}else do{if(this.pos>=this.len)throw indexOutOfRange(this)}while(128&this.buf[this.pos++]);return this},Reader$1.prototype.skipType=function(t){switch(t){case 0:this.skip();break;case 1:this.skip(8);break;case 2:this.skip(this.uint32());break;case 3:for(;4!=(t=7&this.uint32());)this.skipType(t);break;case 5:this.skip(4);break;default:throw Error("invalid wire type "+t+" at offset "+this.pos)}return this},Reader$1._configure=function(t){BufferReader$1=t,Reader$1.create=create(),BufferReader$1._configure();var e=util$2.Long?"toLong":"toNumber";util$2.merge(Reader$1.prototype,{int64:function(){return readLongVarint.call(this)[e](!1)},uint64:function(){return readLongVarint.call(this)[e](!0)},sint64:function(){return readLongVarint.call(this).zzDecode()[e](!1)},fixed64:function(){return readFixed64.call(this)[e](!0)},sfixed64:function(){return readFixed64.call(this)[e](!1)}})};var reader_buffer=BufferReader,Reader=reader;(BufferReader.prototype=Object.create(Reader.prototype)).constructor=BufferReader;var util$1=requireMinimal();function BufferReader(t){Reader.call(this,t)}BufferReader._configure=function(){util$1.Buffer&&(BufferReader.prototype._slice=util$1.Buffer.prototype.slice)},BufferReader.prototype.string=function(){var t=this.uint32();return this.buf.utf8Slice?this.buf.utf8Slice(this.pos,this.pos=Math.min(this.pos+t,this.len)):this.buf.toString("utf-8",this.pos,this.pos=Math.min(this.pos+t,this.len))},BufferReader._configure();var rpc={},service=Service,util=requireMinimal();function Service(t,e,o){if("function"!=typeof t)throw TypeError("rpcImpl must be a function");util.EventEmitter.call(this),this.rpcImpl=t,this.requestDelimited=Boolean(e),this.responseDelimited=Boolean(o)}(Service.prototype=Object.create(util.EventEmitter.prototype)).constructor=Service,Service.prototype.rpcCall=function t(e,o,r,n,i){if(!n)throw TypeError("request must be specified");var s=this;if(!i)return util.asPromise(t,s,e,o,r,n);if(s.rpcImpl)try{return s.rpcImpl(e,o[s.requestDelimited?"encodeDelimited":"encode"](n).finish(),(function(t,o){if(t)return s.emit("error",t,e),i(t);if(null!==o){if(!(o instanceof r))try{o=r[s.responseDelimited?"decodeDelimited":"decode"](o)}catch(t){return s.emit("error",t,e),i(t)}return s.emit("data",o,e),i(null,o)}s.end(!0)}))}catch(t){return s.emit("error",t,e),void setTimeout((function(){i(t)}),0)}else setTimeout((function(){i(Error("already ended"))}),0)},Service.prototype.end=function(t){return this.rpcImpl&&(t||this.rpcImpl(null,null,null),this.rpcImpl=null,this.emit("end").off()),this},function(t){t.Service=service}(rpc);var roots={};!function(t){var e=indexMinimal;function o(){e.util._configure(),e.Writer._configure(e.BufferWriter),e.Reader._configure(e.BufferReader)}e.build="minimal",e.Writer=writer,e.BufferWriter=writer_buffer,e.Reader=reader,e.BufferReader=reader_buffer,e.util=requireMinimal(),e.rpc=rpc,e.roots=roots,e.configure=o,o()}();var minimal=indexMinimal;const $util=minimal.util,$root=minimal.roots.default||(minimal.roots.default={}),gdm=$root.gdm=(()=>{const t={};return t.DataType=function(){const t={},e=Object.create(t);return e[t[0]="unknownType"]=0,e[t[1]="hello"]=1,e[t[2]="event"]=2,e[t[3]="eventAck"]=3,e[t[4]="request"]=4,e[t[5]="response"]=5,e[t[6]="poll"]=6,e[t[7]="pollResponse"]=7,e}(),t.Data=function(){function t(t){if(this.pollResponse=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}let e;return t.prototype.type=0,t.prototype.hello=null,t.prototype.request=null,t.prototype.response=null,t.prototype.event=null,t.prototype.pollResponse=$util.emptyArray,Object.defineProperty(t.prototype,"_hello",{get:$util.oneOfGetter(e=["hello"]),set:$util.oneOfSetter(e)}),Object.defineProperty(t.prototype,"_request",{get:$util.oneOfGetter(e=["request"]),set:$util.oneOfSetter(e)}),Object.defineProperty(t.prototype,"_response",{get:$util.oneOfGetter(e=["response"]),set:$util.oneOfSetter(e)}),Object.defineProperty(t.prototype,"_event",{get:$util.oneOfGetter(e=["event"]),set:$util.oneOfSetter(e)}),t.fromObject=function(t){if(t instanceof $root.gdm.Data)return t;let e=new $root.gdm.Data;switch(t.type){default:if("number"==typeof t.type){e.type=t.type;break}break;case"unknownType":case 0:e.type=0;break;case"hello":case 1:e.type=1;break;case"event":case 2:e.type=2;break;case"eventAck":case 3:e.type=3;break;case"request":case 4:e.type=4;break;case"response":case 5:e.type=5;break;case"poll":case 6:e.type=6;break;case"pollResponse":case 7:e.type=7}if(null!=t.hello){if("object"!=typeof t.hello)throw TypeError(".gdm.Data.hello: object expected");e.hello=$root.gdm.HelloData.fromObject(t.hello)}if(null!=t.request){if("object"!=typeof t.request)throw TypeError(".gdm.Data.request: object expected");e.request=$root.gdm.RequestData.fromObject(t.request)}if(null!=t.response){if("object"!=typeof t.response)throw TypeError(".gdm.Data.response: object expected");e.response=$root.gdm.ResponseData.fromObject(t.response)}if(null!=t.event){if("object"!=typeof t.event)throw TypeError(".gdm.Data.event: object expected");e.event=$root.gdm.EventData.fromObject(t.event)}if(t.pollResponse){if(!Array.isArray(t.pollResponse))throw TypeError(".gdm.Data.pollResponse: array expected");e.pollResponse=[];for(let o=0;o<t.pollResponse.length;++o){if("object"!=typeof t.pollResponse[o])throw TypeError(".gdm.Data.pollResponse: object expected");e.pollResponse[o]=$root.gdm.EventData.fromObject(t.pollResponse[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.pollResponse=[]),e.defaults&&(o.type=e.enums===String?"unknownType":0),null!=t.type&&t.hasOwnProperty("type")&&(o.type=e.enums===String?void 0===$root.gdm.DataType[t.type]?t.type:$root.gdm.DataType[t.type]:t.type),null!=t.hello&&t.hasOwnProperty("hello")&&(o.hello=$root.gdm.HelloData.toObject(t.hello,e),e.oneofs&&(o._hello="hello")),null!=t.request&&t.hasOwnProperty("request")&&(o.request=$root.gdm.RequestData.toObject(t.request,e),e.oneofs&&(o._request="request")),null!=t.response&&t.hasOwnProperty("response")&&(o.response=$root.gdm.ResponseData.toObject(t.response,e),e.oneofs&&(o._response="response")),null!=t.event&&t.hasOwnProperty("event")&&(o.event=$root.gdm.EventData.toObject(t.event,e),e.oneofs&&(o._event="event")),t.pollResponse&&t.pollResponse.length){o.pollResponse=[];for(let r=0;r<t.pollResponse.length;++r)o.pollResponse[r]=$root.gdm.EventData.toObject(t.pollResponse[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.HelloData=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.version=0,t.fromObject=function(t){if(t instanceof $root.gdm.HelloData)return t;let e=new $root.gdm.HelloData;return null!=t.version&&(e.version=t.version>>>0),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.version=0),null!=t.version&&t.hasOwnProperty("version")&&(o.version=t.version),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.RequestType=function(){const t={},e=Object.create(t);return e[t[0]="unknownRequest"]=0,e[t[1]="updateBrokersList"]=1,e[t[2]="composeAuthenticationView"]=2,e[t[3]="uiLayoutCapabilities"]=3,e[t[4]="changeStage"]=4,e}(),t.Requests=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.fromObject=function(t){return t instanceof $root.gdm.Requests?t:new $root.gdm.Requests},t.toObject=function(){return{}},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t.UiLayoutCapabilities=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.fromObject=function(t){return t instanceof $root.gdm.Requests.UiLayoutCapabilities?t:new $root.gdm.Requests.UiLayoutCapabilities},t.toObject=function(){return{}},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.ChangeStage=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.stage=0,t.fromObject=function(t){if(t instanceof $root.gdm.Requests.ChangeStage)return t;let e=new $root.gdm.Requests.ChangeStage;switch(t.stage){default:if("number"==typeof t.stage){e.stage=t.stage;break}break;case"userSelection":case 0:e.stage=0;break;case"brokerSelection":case 1:e.stage=1;break;case"authModeSelection":case 2:e.stage=2;break;case"challenge":case 3:e.stage=3}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.stage=e.enums===String?"userSelection":0),null!=t.stage&&t.hasOwnProperty("stage")&&(o.stage=e.enums===String?void 0===$root.pam.Stage[t.stage]?t.stage:$root.pam.Stage[t.stage]:t.stage),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t}(),t.RequestData=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}let e;return t.prototype.type=0,t.prototype.uiLayoutCapabilities=null,t.prototype.changeStage=null,Object.defineProperty(t.prototype,"data",{get:$util.oneOfGetter(e=["uiLayoutCapabilities","changeStage"]),set:$util.oneOfSetter(e)}),t.fromObject=function(t){if(t instanceof $root.gdm.RequestData)return t;let e=new $root.gdm.RequestData;switch(t.type){default:if("number"==typeof t.type){e.type=t.type;break}break;case"unknownRequest":case 0:e.type=0;break;case"updateBrokersList":case 1:e.type=1;break;case"composeAuthenticationView":case 2:e.type=2;break;case"uiLayoutCapabilities":case 3:e.type=3;break;case"changeStage":case 4:e.type=4}if(null!=t.uiLayoutCapabilities){if("object"!=typeof t.uiLayoutCapabilities)throw TypeError(".gdm.RequestData.uiLayoutCapabilities: object expected");e.uiLayoutCapabilities=$root.gdm.Requests.UiLayoutCapabilities.fromObject(t.uiLayoutCapabilities)}if(null!=t.changeStage){if("object"!=typeof t.changeStage)throw TypeError(".gdm.RequestData.changeStage: object expected");e.changeStage=$root.gdm.Requests.ChangeStage.fromObject(t.changeStage)}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.type=e.enums===String?"unknownRequest":0),null!=t.type&&t.hasOwnProperty("type")&&(o.type=e.enums===String?void 0===$root.gdm.RequestType[t.type]?t.type:$root.gdm.RequestType[t.type]:t.type),null!=t.uiLayoutCapabilities&&t.hasOwnProperty("uiLayoutCapabilities")&&(o.uiLayoutCapabilities=$root.gdm.Requests.UiLayoutCapabilities.toObject(t.uiLayoutCapabilities,e),e.oneofs&&(o.data="uiLayoutCapabilities")),null!=t.changeStage&&t.hasOwnProperty("changeStage")&&(o.changeStage=$root.gdm.Requests.ChangeStage.toObject(t.changeStage,e),e.oneofs&&(o.data="changeStage")),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.Responses=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.fromObject=function(t){return t instanceof $root.gdm.Responses?t:new $root.gdm.Responses},t.toObject=function(){return{}},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t.Ack=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.fromObject=function(t){return t instanceof $root.gdm.Responses.Ack?t:new $root.gdm.Responses.Ack},t.toObject=function(){return{}},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.UiLayoutCapabilities=function(){function t(t){if(this.supportedUiLayouts=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.supportedUiLayouts=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.gdm.Responses.UiLayoutCapabilities)return t;let e=new $root.gdm.Responses.UiLayoutCapabilities;if(t.supportedUiLayouts){if(!Array.isArray(t.supportedUiLayouts))throw TypeError(".gdm.Responses.UiLayoutCapabilities.supportedUiLayouts: array expected");e.supportedUiLayouts=[];for(let o=0;o<t.supportedUiLayouts.length;++o){if("object"!=typeof t.supportedUiLayouts[o])throw TypeError(".gdm.Responses.UiLayoutCapabilities.supportedUiLayouts: object expected");e.supportedUiLayouts[o]=$root.authd.UILayout.fromObject(t.supportedUiLayouts[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.supportedUiLayouts=[]),t.supportedUiLayouts&&t.supportedUiLayouts.length){o.supportedUiLayouts=[];for(let r=0;r<t.supportedUiLayouts.length;++r)o.supportedUiLayouts[r]=$root.authd.UILayout.toObject(t.supportedUiLayouts[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t}(),t.ResponseData=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}let e;return t.prototype.type=0,t.prototype.ack=null,t.prototype.uiLayoutCapabilities=null,Object.defineProperty(t.prototype,"data",{get:$util.oneOfGetter(e=["ack","uiLayoutCapabilities"]),set:$util.oneOfSetter(e)}),t.fromObject=function(t){if(t instanceof $root.gdm.ResponseData)return t;let e=new $root.gdm.ResponseData;switch(t.type){default:if("number"==typeof t.type){e.type=t.type;break}break;case"unknownRequest":case 0:e.type=0;break;case"updateBrokersList":case 1:e.type=1;break;case"composeAuthenticationView":case 2:e.type=2;break;case"uiLayoutCapabilities":case 3:e.type=3;break;case"changeStage":case 4:e.type=4}if(null!=t.ack){if("object"!=typeof t.ack)throw TypeError(".gdm.ResponseData.ack: object expected");e.ack=$root.gdm.Responses.Ack.fromObject(t.ack)}if(null!=t.uiLayoutCapabilities){if("object"!=typeof t.uiLayoutCapabilities)throw TypeError(".gdm.ResponseData.uiLayoutCapabilities: object expected");e.uiLayoutCapabilities=$root.gdm.Responses.UiLayoutCapabilities.fromObject(t.uiLayoutCapabilities)}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.type=e.enums===String?"unknownRequest":0),null!=t.type&&t.hasOwnProperty("type")&&(o.type=e.enums===String?void 0===$root.gdm.RequestType[t.type]?t.type:$root.gdm.RequestType[t.type]:t.type),null!=t.ack&&t.hasOwnProperty("ack")&&(o.ack=$root.gdm.Responses.Ack.toObject(t.ack,e),e.oneofs&&(o.data="ack")),null!=t.uiLayoutCapabilities&&t.hasOwnProperty("uiLayoutCapabilities")&&(o.uiLayoutCapabilities=$root.gdm.Responses.UiLayoutCapabilities.toObject(t.uiLayoutCapabilities,e),e.oneofs&&(o.data="uiLayoutCapabilities")),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.EventType=function(){const t={},e=Object.create(t);return e[t[0]="unknownEvent"]=0,e[t[1]="userSelected"]=1,e[t[2]="brokersReceived"]=2,e[t[3]="brokerSelected"]=3,e[t[4]="authModesReceived"]=4,e[t[5]="authModeSelected"]=5,e[t[6]="reselectAuthMode"]=6,e[t[7]="authEvent"]=7,e[t[8]="uiLayoutReceived"]=8,e[t[9]="startAuthentication"]=9,e[t[10]="isAuthenticatedRequested"]=10,e[t[12]="stageChanged"]=12,e[t[11]="isAuthenticatedCancelled"]=11,e}(),t.Events=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.fromObject=function(t){return t instanceof $root.gdm.Events?t:new $root.gdm.Events},t.toObject=function(){return{}},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t.BrokersReceived=function(){function t(t){if(this.brokersInfos=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.brokersInfos=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.gdm.Events.BrokersReceived)return t;let e=new $root.gdm.Events.BrokersReceived;if(t.brokersInfos){if(!Array.isArray(t.brokersInfos))throw TypeError(".gdm.Events.BrokersReceived.brokersInfos: array expected");e.brokersInfos=[];for(let o=0;o<t.brokersInfos.length;++o){if("object"!=typeof t.brokersInfos[o])throw TypeError(".gdm.Events.BrokersReceived.brokersInfos: object expected");e.brokersInfos[o]=$root.authd.ABResponse.BrokerInfo.fromObject(t.brokersInfos[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.brokersInfos=[]),t.brokersInfos&&t.brokersInfos.length){o.brokersInfos=[];for(let r=0;r<t.brokersInfos.length;++r)o.brokersInfos[r]=$root.authd.ABResponse.BrokerInfo.toObject(t.brokersInfos[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.BrokerSelected=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.brokerId="",t.fromObject=function(t){if(t instanceof $root.gdm.Events.BrokerSelected)return t;let e=new $root.gdm.Events.BrokerSelected;return null!=t.brokerId&&(e.brokerId=String(t.brokerId)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.brokerId=""),null!=t.brokerId&&t.hasOwnProperty("brokerId")&&(o.brokerId=t.brokerId),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.UserSelected=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.userId="",t.fromObject=function(t){if(t instanceof $root.gdm.Events.UserSelected)return t;let e=new $root.gdm.Events.UserSelected;return null!=t.userId&&(e.userId=String(t.userId)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.userId=""),null!=t.userId&&t.hasOwnProperty("userId")&&(o.userId=t.userId),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.StartAuthentication=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.fromObject=function(t){return t instanceof $root.gdm.Events.StartAuthentication?t:new $root.gdm.Events.StartAuthentication},t.toObject=function(){return{}},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.AuthModesReceived=function(){function t(t){if(this.authModes=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.authModes=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.gdm.Events.AuthModesReceived)return t;let e=new $root.gdm.Events.AuthModesReceived;if(t.authModes){if(!Array.isArray(t.authModes))throw TypeError(".gdm.Events.AuthModesReceived.authModes: array expected");e.authModes=[];for(let o=0;o<t.authModes.length;++o){if("object"!=typeof t.authModes[o])throw TypeError(".gdm.Events.AuthModesReceived.authModes: object expected");e.authModes[o]=$root.authd.GAMResponse.AuthenticationMode.fromObject(t.authModes[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.authModes=[]),t.authModes&&t.authModes.length){o.authModes=[];for(let r=0;r<t.authModes.length;++r)o.authModes[r]=$root.authd.GAMResponse.AuthenticationMode.toObject(t.authModes[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.AuthModeSelected=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.authModeId="",t.fromObject=function(t){if(t instanceof $root.gdm.Events.AuthModeSelected)return t;let e=new $root.gdm.Events.AuthModeSelected;return null!=t.authModeId&&(e.authModeId=String(t.authModeId)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.authModeId=""),null!=t.authModeId&&t.hasOwnProperty("authModeId")&&(o.authModeId=t.authModeId),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.AuthEvent=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.response=null,t.fromObject=function(t){if(t instanceof $root.gdm.Events.AuthEvent)return t;let e=new $root.gdm.Events.AuthEvent;if(null!=t.response){if("object"!=typeof t.response)throw TypeError(".gdm.Events.AuthEvent.response: object expected");e.response=$root.authd.IAResponse.fromObject(t.response)}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.response=null),null!=t.response&&t.hasOwnProperty("response")&&(o.response=$root.authd.IAResponse.toObject(t.response,e)),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.ReselectAuthMode=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.fromObject=function(t){return t instanceof $root.gdm.Events.ReselectAuthMode?t:new $root.gdm.Events.ReselectAuthMode},t.toObject=function(){return{}},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.IsAuthenticatedRequested=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.authenticationData=null,t.fromObject=function(t){if(t instanceof $root.gdm.Events.IsAuthenticatedRequested)return t;let e=new $root.gdm.Events.IsAuthenticatedRequested;if(null!=t.authenticationData){if("object"!=typeof t.authenticationData)throw TypeError(".gdm.Events.IsAuthenticatedRequested.authenticationData: object expected");e.authenticationData=$root.authd.IARequest.AuthenticationData.fromObject(t.authenticationData)}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.authenticationData=null),null!=t.authenticationData&&t.hasOwnProperty("authenticationData")&&(o.authenticationData=$root.authd.IARequest.AuthenticationData.toObject(t.authenticationData,e)),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.IsAuthenticatedCancelled=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.fromObject=function(t){return t instanceof $root.gdm.Events.IsAuthenticatedCancelled?t:new $root.gdm.Events.IsAuthenticatedCancelled},t.toObject=function(){return{}},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.StageChanged=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.stage=0,t.fromObject=function(t){if(t instanceof $root.gdm.Events.StageChanged)return t;let e=new $root.gdm.Events.StageChanged;switch(t.stage){default:if("number"==typeof t.stage){e.stage=t.stage;break}break;case"userSelection":case 0:e.stage=0;break;case"brokerSelection":case 1:e.stage=1;break;case"authModeSelection":case 2:e.stage=2;break;case"challenge":case 3:e.stage=3}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.stage=e.enums===String?"userSelection":0),null!=t.stage&&t.hasOwnProperty("stage")&&(o.stage=e.enums===String?void 0===$root.pam.Stage[t.stage]?t.stage:$root.pam.Stage[t.stage]:t.stage),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.UiLayoutReceived=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.uiLayout=null,t.fromObject=function(t){if(t instanceof $root.gdm.Events.UiLayoutReceived)return t;let e=new $root.gdm.Events.UiLayoutReceived;if(null!=t.uiLayout){if("object"!=typeof t.uiLayout)throw TypeError(".gdm.Events.UiLayoutReceived.uiLayout: object expected");e.uiLayout=$root.authd.UILayout.fromObject(t.uiLayout)}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.uiLayout=null),null!=t.uiLayout&&t.hasOwnProperty("uiLayout")&&(o.uiLayout=$root.authd.UILayout.toObject(t.uiLayout,e)),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t}(),t.EventData=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}let e;return t.prototype.type=0,t.prototype.brokersReceived=null,t.prototype.brokerSelected=null,t.prototype.authModesReceived=null,t.prototype.authModeSelected=null,t.prototype.isAuthenticatedRequested=null,t.prototype.stageChanged=null,t.prototype.uiLayoutReceived=null,t.prototype.authEvent=null,t.prototype.reselectAuthMode=null,t.prototype.startAuthentication=null,t.prototype.userSelected=null,t.prototype.isAuthenticatedCancelled=null,Object.defineProperty(t.prototype,"data",{get:$util.oneOfGetter(e=["brokersReceived","brokerSelected","authModesReceived","authModeSelected","isAuthenticatedRequested","stageChanged","uiLayoutReceived","authEvent","reselectAuthMode","startAuthentication","userSelected","isAuthenticatedCancelled"]),set:$util.oneOfSetter(e)}),t.fromObject=function(t){if(t instanceof $root.gdm.EventData)return t;let e=new $root.gdm.EventData;switch(t.type){default:if("number"==typeof t.type){e.type=t.type;break}break;case"unknownEvent":case 0:e.type=0;break;case"userSelected":case 1:e.type=1;break;case"brokersReceived":case 2:e.type=2;break;case"brokerSelected":case 3:e.type=3;break;case"authModesReceived":case 4:e.type=4;break;case"authModeSelected":case 5:e.type=5;break;case"reselectAuthMode":case 6:e.type=6;break;case"authEvent":case 7:e.type=7;break;case"uiLayoutReceived":case 8:e.type=8;break;case"startAuthentication":case 9:e.type=9;break;case"isAuthenticatedRequested":case 10:e.type=10;break;case"stageChanged":case 12:e.type=12;break;case"isAuthenticatedCancelled":case 11:e.type=11}if(null!=t.brokersReceived){if("object"!=typeof t.brokersReceived)throw TypeError(".gdm.EventData.brokersReceived: object expected");e.brokersReceived=$root.gdm.Events.BrokersReceived.fromObject(t.brokersReceived)}if(null!=t.brokerSelected){if("object"!=typeof t.brokerSelected)throw TypeError(".gdm.EventData.brokerSelected: object expected");e.brokerSelected=$root.gdm.Events.BrokerSelected.fromObject(t.brokerSelected)}if(null!=t.authModesReceived){if("object"!=typeof t.authModesReceived)throw TypeError(".gdm.EventData.authModesReceived: object expected");e.authModesReceived=$root.gdm.Events.AuthModesReceived.fromObject(t.authModesReceived)}if(null!=t.authModeSelected){if("object"!=typeof t.authModeSelected)throw TypeError(".gdm.EventData.authModeSelected: object expected");e.authModeSelected=$root.gdm.Events.AuthModeSelected.fromObject(t.authModeSelected)}if(null!=t.isAuthenticatedRequested){if("object"!=typeof t.isAuthenticatedRequested)throw TypeError(".gdm.EventData.isAuthenticatedRequested: object expected");e.isAuthenticatedRequested=$root.gdm.Events.IsAuthenticatedRequested.fromObject(t.isAuthenticatedRequested)}if(null!=t.stageChanged){if("object"!=typeof t.stageChanged)throw TypeError(".gdm.EventData.stageChanged: object expected");e.stageChanged=$root.gdm.Events.StageChanged.fromObject(t.stageChanged)}if(null!=t.uiLayoutReceived){if("object"!=typeof t.uiLayoutReceived)throw TypeError(".gdm.EventData.uiLayoutReceived: object expected");e.uiLayoutReceived=$root.gdm.Events.UiLayoutReceived.fromObject(t.uiLayoutReceived)}if(null!=t.authEvent){if("object"!=typeof t.authEvent)throw TypeError(".gdm.EventData.authEvent: object expected");e.authEvent=$root.gdm.Events.AuthEvent.fromObject(t.authEvent)}if(null!=t.reselectAuthMode){if("object"!=typeof t.reselectAuthMode)throw TypeError(".gdm.EventData.reselectAuthMode: object expected");e.reselectAuthMode=$root.gdm.Events.ReselectAuthMode.fromObject(t.reselectAuthMode)}if(null!=t.startAuthentication){if("object"!=typeof t.startAuthentication)throw TypeError(".gdm.EventData.startAuthentication: object expected");e.startAuthentication=$root.gdm.Events.StartAuthentication.fromObject(t.startAuthentication)}if(null!=t.userSelected){if("object"!=typeof t.userSelected)throw TypeError(".gdm.EventData.userSelected: object expected");e.userSelected=$root.gdm.Events.UserSelected.fromObject(t.userSelected)}if(null!=t.isAuthenticatedCancelled){if("object"!=typeof t.isAuthenticatedCancelled)throw TypeError(".gdm.EventData.isAuthenticatedCancelled: object expected");e.isAuthenticatedCancelled=$root.gdm.Events.IsAuthenticatedCancelled.fromObject(t.isAuthenticatedCancelled)}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.type=e.enums===String?"unknownEvent":0),null!=t.type&&t.hasOwnProperty("type")&&(o.type=e.enums===String?void 0===$root.gdm.EventType[t.type]?t.type:$root.gdm.EventType[t.type]:t.type),null!=t.brokersReceived&&t.hasOwnProperty("brokersReceived")&&(o.brokersReceived=$root.gdm.Events.BrokersReceived.toObject(t.brokersReceived,e),e.oneofs&&(o.data="brokersReceived")),null!=t.brokerSelected&&t.hasOwnProperty("brokerSelected")&&(o.brokerSelected=$root.gdm.Events.BrokerSelected.toObject(t.brokerSelected,e),e.oneofs&&(o.data="brokerSelected")),null!=t.authModesReceived&&t.hasOwnProperty("authModesReceived")&&(o.authModesReceived=$root.gdm.Events.AuthModesReceived.toObject(t.authModesReceived,e),e.oneofs&&(o.data="authModesReceived")),null!=t.authModeSelected&&t.hasOwnProperty("authModeSelected")&&(o.authModeSelected=$root.gdm.Events.AuthModeSelected.toObject(t.authModeSelected,e),e.oneofs&&(o.data="authModeSelected")),null!=t.isAuthenticatedRequested&&t.hasOwnProperty("isAuthenticatedRequested")&&(o.isAuthenticatedRequested=$root.gdm.Events.IsAuthenticatedRequested.toObject(t.isAuthenticatedRequested,e),e.oneofs&&(o.data="isAuthenticatedRequested")),null!=t.stageChanged&&t.hasOwnProperty("stageChanged")&&(o.stageChanged=$root.gdm.Events.StageChanged.toObject(t.stageChanged,e),e.oneofs&&(o.data="stageChanged")),null!=t.uiLayoutReceived&&t.hasOwnProperty("uiLayoutReceived")&&(o.uiLayoutReceived=$root.gdm.Events.UiLayoutReceived.toObject(t.uiLayoutReceived,e),e.oneofs&&(o.data="uiLayoutReceived")),null!=t.authEvent&&t.hasOwnProperty("authEvent")&&(o.authEvent=$root.gdm.Events.AuthEvent.toObject(t.authEvent,e),e.oneofs&&(o.data="authEvent")),null!=t.reselectAuthMode&&t.hasOwnProperty("reselectAuthMode")&&(o.reselectAuthMode=$root.gdm.Events.ReselectAuthMode.toObject(t.reselectAuthMode,e),e.oneofs&&(o.data="reselectAuthMode")),null!=t.startAuthentication&&t.hasOwnProperty("startAuthentication")&&(o.startAuthentication=$root.gdm.Events.StartAuthentication.toObject(t.startAuthentication,e),e.oneofs&&(o.data="startAuthentication")),null!=t.userSelected&&t.hasOwnProperty("userSelected")&&(o.userSelected=$root.gdm.Events.UserSelected.toObject(t.userSelected,e),e.oneofs&&(o.data="userSelected")),null!=t.isAuthenticatedCancelled&&t.hasOwnProperty("isAuthenticatedCancelled")&&(o.isAuthenticatedCancelled=$root.gdm.Events.IsAuthenticatedCancelled.toObject(t.isAuthenticatedCancelled,e),e.oneofs&&(o.data="isAuthenticatedCancelled")),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t})(),authd=$root.authd=(()=>{const t={};return t.Empty=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.fromObject=function(t){return t instanceof $root.authd.Empty?t:new $root.authd.Empty},t.toObject=function(){return{}},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GPBRequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.username="",t.fromObject=function(t){if(t instanceof $root.authd.GPBRequest)return t;let e=new $root.authd.GPBRequest;return null!=t.username&&(e.username=String(t.username)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.username=""),null!=t.username&&t.hasOwnProperty("username")&&(o.username=t.username),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GPBResponse=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}let e;return t.prototype.previousBroker=null,Object.defineProperty(t.prototype,"_previousBroker",{get:$util.oneOfGetter(e=["previousBroker"]),set:$util.oneOfSetter(e)}),t.fromObject=function(t){if(t instanceof $root.authd.GPBResponse)return t;let e=new $root.authd.GPBResponse;return null!=t.previousBroker&&(e.previousBroker=String(t.previousBroker)),e},t.toObject=function(t,e){e||(e={});let o={};return null!=t.previousBroker&&t.hasOwnProperty("previousBroker")&&(o.previousBroker=t.previousBroker,e.oneofs&&(o._previousBroker="previousBroker")),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.ABResponse=function(){function t(t){if(this.brokersInfos=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.brokersInfos=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.authd.ABResponse)return t;let e=new $root.authd.ABResponse;if(t.brokersInfos){if(!Array.isArray(t.brokersInfos))throw TypeError(".authd.ABResponse.brokersInfos: array expected");e.brokersInfos=[];for(let o=0;o<t.brokersInfos.length;++o){if("object"!=typeof t.brokersInfos[o])throw TypeError(".authd.ABResponse.brokersInfos: object expected");e.brokersInfos[o]=$root.authd.ABResponse.BrokerInfo.fromObject(t.brokersInfos[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.brokersInfos=[]),t.brokersInfos&&t.brokersInfos.length){o.brokersInfos=[];for(let r=0;r<t.brokersInfos.length;++r)o.brokersInfos[r]=$root.authd.ABResponse.BrokerInfo.toObject(t.brokersInfos[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t.BrokerInfo=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}let e;return t.prototype.id="",t.prototype.name="",t.prototype.brandIcon=null,Object.defineProperty(t.prototype,"_brandIcon",{get:$util.oneOfGetter(e=["brandIcon"]),set:$util.oneOfSetter(e)}),t.fromObject=function(t){if(t instanceof $root.authd.ABResponse.BrokerInfo)return t;let e=new $root.authd.ABResponse.BrokerInfo;return null!=t.id&&(e.id=String(t.id)),null!=t.name&&(e.name=String(t.name)),null!=t.brandIcon&&(e.brandIcon=String(t.brandIcon)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.id="",o.name=""),null!=t.id&&t.hasOwnProperty("id")&&(o.id=t.id),null!=t.name&&t.hasOwnProperty("name")&&(o.name=t.name),null!=t.brandIcon&&t.hasOwnProperty("brandIcon")&&(o.brandIcon=t.brandIcon,e.oneofs&&(o._brandIcon="brandIcon")),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t}(),t.StringResponse=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.msg="",t.fromObject=function(t){if(t instanceof $root.authd.StringResponse)return t;let e=new $root.authd.StringResponse;return null!=t.msg&&(e.msg=String(t.msg)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.msg=""),null!=t.msg&&t.hasOwnProperty("msg")&&(o.msg=t.msg),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.SessionMode=function(){const t={},e=Object.create(t);return e[t[0]="UNDEFINED"]=0,e[t[1]="AUTH"]=1,e[t[2]="PASSWD"]=2,e}(),t.SBRequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.brokerId="",t.prototype.username="",t.prototype.lang="",t.prototype.mode=0,t.fromObject=function(t){if(t instanceof $root.authd.SBRequest)return t;let e=new $root.authd.SBRequest;switch(null!=t.brokerId&&(e.brokerId=String(t.brokerId)),null!=t.username&&(e.username=String(t.username)),null!=t.lang&&(e.lang=String(t.lang)),t.mode){default:if("number"==typeof t.mode){e.mode=t.mode;break}break;case"UNDEFINED":case 0:e.mode=0;break;case"AUTH":case 1:e.mode=1;break;case"PASSWD":case 2:e.mode=2}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.brokerId="",o.username="",o.lang="",o.mode=e.enums===String?"UNDEFINED":0),null!=t.brokerId&&t.hasOwnProperty("brokerId")&&(o.brokerId=t.brokerId),null!=t.username&&t.hasOwnProperty("username")&&(o.username=t.username),null!=t.lang&&t.hasOwnProperty("lang")&&(o.lang=t.lang),null!=t.mode&&t.hasOwnProperty("mode")&&(o.mode=e.enums===String?void 0===$root.authd.SessionMode[t.mode]?t.mode:$root.authd.SessionMode[t.mode]:t.mode),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.SBResponse=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.sessionId="",t.prototype.encryptionKey="",t.fromObject=function(t){if(t instanceof $root.authd.SBResponse)return t;let e=new $root.authd.SBResponse;return null!=t.sessionId&&(e.sessionId=String(t.sessionId)),null!=t.encryptionKey&&(e.encryptionKey=String(t.encryptionKey)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.sessionId="",o.encryptionKey=""),null!=t.sessionId&&t.hasOwnProperty("sessionId")&&(o.sessionId=t.sessionId),null!=t.encryptionKey&&t.hasOwnProperty("encryptionKey")&&(o.encryptionKey=t.encryptionKey),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GAMRequest=function(){function t(t){if(this.supportedUiLayouts=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.sessionId="",t.prototype.supportedUiLayouts=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.authd.GAMRequest)return t;let e=new $root.authd.GAMRequest;if(null!=t.sessionId&&(e.sessionId=String(t.sessionId)),t.supportedUiLayouts){if(!Array.isArray(t.supportedUiLayouts))throw TypeError(".authd.GAMRequest.supportedUiLayouts: array expected");e.supportedUiLayouts=[];for(let o=0;o<t.supportedUiLayouts.length;++o){if("object"!=typeof t.supportedUiLayouts[o])throw TypeError(".authd.GAMRequest.supportedUiLayouts: object expected");e.supportedUiLayouts[o]=$root.authd.UILayout.fromObject(t.supportedUiLayouts[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.supportedUiLayouts=[]),e.defaults&&(o.sessionId=""),null!=t.sessionId&&t.hasOwnProperty("sessionId")&&(o.sessionId=t.sessionId),t.supportedUiLayouts&&t.supportedUiLayouts.length){o.supportedUiLayouts=[];for(let r=0;r<t.supportedUiLayouts.length;++r)o.supportedUiLayouts[r]=$root.authd.UILayout.toObject(t.supportedUiLayouts[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.UILayout=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}let e;return t.prototype.type="",t.prototype.label=null,t.prototype.button=null,t.prototype.wait=null,t.prototype.entry=null,t.prototype.content=null,Object.defineProperty(t.prototype,"_label",{get:$util.oneOfGetter(e=["label"]),set:$util.oneOfSetter(e)}),Object.defineProperty(t.prototype,"_button",{get:$util.oneOfGetter(e=["button"]),set:$util.oneOfSetter(e)}),Object.defineProperty(t.prototype,"_wait",{get:$util.oneOfGetter(e=["wait"]),set:$util.oneOfSetter(e)}),Object.defineProperty(t.prototype,"_entry",{get:$util.oneOfGetter(e=["entry"]),set:$util.oneOfSetter(e)}),Object.defineProperty(t.prototype,"_content",{get:$util.oneOfGetter(e=["content"]),set:$util.oneOfSetter(e)}),t.fromObject=function(t){if(t instanceof $root.authd.UILayout)return t;let e=new $root.authd.UILayout;return null!=t.type&&(e.type=String(t.type)),null!=t.label&&(e.label=String(t.label)),null!=t.button&&(e.button=String(t.button)),null!=t.wait&&(e.wait=String(t.wait)),null!=t.entry&&(e.entry=String(t.entry)),null!=t.content&&(e.content=String(t.content)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.type=""),null!=t.type&&t.hasOwnProperty("type")&&(o.type=t.type),null!=t.label&&t.hasOwnProperty("label")&&(o.label=t.label,e.oneofs&&(o._label="label")),null!=t.button&&t.hasOwnProperty("button")&&(o.button=t.button,e.oneofs&&(o._button="button")),null!=t.wait&&t.hasOwnProperty("wait")&&(o.wait=t.wait,e.oneofs&&(o._wait="wait")),null!=t.entry&&t.hasOwnProperty("entry")&&(o.entry=t.entry,e.oneofs&&(o._entry="entry")),null!=t.content&&t.hasOwnProperty("content")&&(o.content=t.content,e.oneofs&&(o._content="content")),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GAMResponse=function(){function t(t){if(this.authenticationModes=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.authenticationModes=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.authd.GAMResponse)return t;let e=new $root.authd.GAMResponse;if(t.authenticationModes){if(!Array.isArray(t.authenticationModes))throw TypeError(".authd.GAMResponse.authenticationModes: array expected");e.authenticationModes=[];for(let o=0;o<t.authenticationModes.length;++o){if("object"!=typeof t.authenticationModes[o])throw TypeError(".authd.GAMResponse.authenticationModes: object expected");e.authenticationModes[o]=$root.authd.GAMResponse.AuthenticationMode.fromObject(t.authenticationModes[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.authenticationModes=[]),t.authenticationModes&&t.authenticationModes.length){o.authenticationModes=[];for(let r=0;r<t.authenticationModes.length;++r)o.authenticationModes[r]=$root.authd.GAMResponse.AuthenticationMode.toObject(t.authenticationModes[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t.AuthenticationMode=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.id="",t.prototype.label="",t.fromObject=function(t){if(t instanceof $root.authd.GAMResponse.AuthenticationMode)return t;let e=new $root.authd.GAMResponse.AuthenticationMode;return null!=t.id&&(e.id=String(t.id)),null!=t.label&&(e.label=String(t.label)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.id="",o.label=""),null!=t.id&&t.hasOwnProperty("id")&&(o.id=t.id),null!=t.label&&t.hasOwnProperty("label")&&(o.label=t.label),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t}(),t.SAMRequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.sessionId="",t.prototype.authenticationModeId="",t.fromObject=function(t){if(t instanceof $root.authd.SAMRequest)return t;let e=new $root.authd.SAMRequest;return null!=t.sessionId&&(e.sessionId=String(t.sessionId)),null!=t.authenticationModeId&&(e.authenticationModeId=String(t.authenticationModeId)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.sessionId="",o.authenticationModeId=""),null!=t.sessionId&&t.hasOwnProperty("sessionId")&&(o.sessionId=t.sessionId),null!=t.authenticationModeId&&t.hasOwnProperty("authenticationModeId")&&(o.authenticationModeId=t.authenticationModeId),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.SAMResponse=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.uiLayoutInfo=null,t.fromObject=function(t){if(t instanceof $root.authd.SAMResponse)return t;let e=new $root.authd.SAMResponse;if(null!=t.uiLayoutInfo){if("object"!=typeof t.uiLayoutInfo)throw TypeError(".authd.SAMResponse.uiLayoutInfo: object expected");e.uiLayoutInfo=$root.authd.UILayout.fromObject(t.uiLayoutInfo)}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.uiLayoutInfo=null),null!=t.uiLayoutInfo&&t.hasOwnProperty("uiLayoutInfo")&&(o.uiLayoutInfo=$root.authd.UILayout.toObject(t.uiLayoutInfo,e)),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.IARequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.sessionId="",t.prototype.authenticationData=null,t.fromObject=function(t){if(t instanceof $root.authd.IARequest)return t;let e=new $root.authd.IARequest;if(null!=t.sessionId&&(e.sessionId=String(t.sessionId)),null!=t.authenticationData){if("object"!=typeof t.authenticationData)throw TypeError(".authd.IARequest.authenticationData: object expected");e.authenticationData=$root.authd.IARequest.AuthenticationData.fromObject(t.authenticationData)}return e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.sessionId="",o.authenticationData=null),null!=t.sessionId&&t.hasOwnProperty("sessionId")&&(o.sessionId=t.sessionId),null!=t.authenticationData&&t.hasOwnProperty("authenticationData")&&(o.authenticationData=$root.authd.IARequest.AuthenticationData.toObject(t.authenticationData,e)),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t.AuthenticationData=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}let e;return t.prototype.challenge=null,t.prototype.wait=null,t.prototype.skip=null,Object.defineProperty(t.prototype,"item",{get:$util.oneOfGetter(e=["challenge","wait","skip"]),set:$util.oneOfSetter(e)}),t.fromObject=function(t){if(t instanceof $root.authd.IARequest.AuthenticationData)return t;let e=new $root.authd.IARequest.AuthenticationData;return null!=t.challenge&&(e.challenge=String(t.challenge)),null!=t.wait&&(e.wait=String(t.wait)),null!=t.skip&&(e.skip=String(t.skip)),e},t.toObject=function(t,e){e||(e={});let o={};return null!=t.challenge&&t.hasOwnProperty("challenge")&&(o.challenge=t.challenge,e.oneofs&&(o.item="challenge")),null!=t.wait&&t.hasOwnProperty("wait")&&(o.wait=t.wait,e.oneofs&&(o.item="wait")),null!=t.skip&&t.hasOwnProperty("skip")&&(o.skip=t.skip,e.oneofs&&(o.item="skip")),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t}(),t.IAResponse=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.access="",t.prototype.msg="",t.fromObject=function(t){if(t instanceof $root.authd.IAResponse)return t;let e=new $root.authd.IAResponse;return null!=t.access&&(e.access=String(t.access)),null!=t.msg&&(e.msg=String(t.msg)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.access="",o.msg=""),null!=t.access&&t.hasOwnProperty("access")&&(o.access=t.access),null!=t.msg&&t.hasOwnProperty("msg")&&(o.msg=t.msg),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.SDBFURequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.brokerId="",t.prototype.username="",t.fromObject=function(t){if(t instanceof $root.authd.SDBFURequest)return t;let e=new $root.authd.SDBFURequest;return null!=t.brokerId&&(e.brokerId=String(t.brokerId)),null!=t.username&&(e.username=String(t.username)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.brokerId="",o.username=""),null!=t.brokerId&&t.hasOwnProperty("brokerId")&&(o.brokerId=t.brokerId),null!=t.username&&t.hasOwnProperty("username")&&(o.username=t.username),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.ESRequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.sessionId="",t.fromObject=function(t){if(t instanceof $root.authd.ESRequest)return t;let e=new $root.authd.ESRequest;return null!=t.sessionId&&(e.sessionId=String(t.sessionId)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.sessionId=""),null!=t.sessionId&&t.hasOwnProperty("sessionId")&&(o.sessionId=t.sessionId),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GetPasswdByNameRequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.name="",t.prototype.shouldPreCheck=!1,t.fromObject=function(t){if(t instanceof $root.authd.GetPasswdByNameRequest)return t;let e=new $root.authd.GetPasswdByNameRequest;return null!=t.name&&(e.name=String(t.name)),null!=t.shouldPreCheck&&(e.shouldPreCheck=Boolean(t.shouldPreCheck)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.name="",o.shouldPreCheck=!1),null!=t.name&&t.hasOwnProperty("name")&&(o.name=t.name),null!=t.shouldPreCheck&&t.hasOwnProperty("shouldPreCheck")&&(o.shouldPreCheck=t.shouldPreCheck),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GetGroupByNameRequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.name="",t.fromObject=function(t){if(t instanceof $root.authd.GetGroupByNameRequest)return t;let e=new $root.authd.GetGroupByNameRequest;return null!=t.name&&(e.name=String(t.name)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.name=""),null!=t.name&&t.hasOwnProperty("name")&&(o.name=t.name),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GetShadowByNameRequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.name="",t.fromObject=function(t){if(t instanceof $root.authd.GetShadowByNameRequest)return t;let e=new $root.authd.GetShadowByNameRequest;return null!=t.name&&(e.name=String(t.name)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.name=""),null!=t.name&&t.hasOwnProperty("name")&&(o.name=t.name),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GetByIDRequest=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.id=0,t.fromObject=function(t){if(t instanceof $root.authd.GetByIDRequest)return t;let e=new $root.authd.GetByIDRequest;return null!=t.id&&(e.id=t.id>>>0),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.id=0),null!=t.id&&t.hasOwnProperty("id")&&(o.id=t.id),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.PasswdEntry=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.name="",t.prototype.passwd="",t.prototype.uid=0,t.prototype.gid=0,t.prototype.gecos="",t.prototype.homedir="",t.prototype.shell="",t.fromObject=function(t){if(t instanceof $root.authd.PasswdEntry)return t;let e=new $root.authd.PasswdEntry;return null!=t.name&&(e.name=String(t.name)),null!=t.passwd&&(e.passwd=String(t.passwd)),null!=t.uid&&(e.uid=t.uid>>>0),null!=t.gid&&(e.gid=t.gid>>>0),null!=t.gecos&&(e.gecos=String(t.gecos)),null!=t.homedir&&(e.homedir=String(t.homedir)),null!=t.shell&&(e.shell=String(t.shell)),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.name="",o.passwd="",o.uid=0,o.gid=0,o.gecos="",o.homedir="",o.shell=""),null!=t.name&&t.hasOwnProperty("name")&&(o.name=t.name),null!=t.passwd&&t.hasOwnProperty("passwd")&&(o.passwd=t.passwd),null!=t.uid&&t.hasOwnProperty("uid")&&(o.uid=t.uid),null!=t.gid&&t.hasOwnProperty("gid")&&(o.gid=t.gid),null!=t.gecos&&t.hasOwnProperty("gecos")&&(o.gecos=t.gecos),null!=t.homedir&&t.hasOwnProperty("homedir")&&(o.homedir=t.homedir),null!=t.shell&&t.hasOwnProperty("shell")&&(o.shell=t.shell),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.PasswdEntries=function(){function t(t){if(this.entries=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.entries=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.authd.PasswdEntries)return t;let e=new $root.authd.PasswdEntries;if(t.entries){if(!Array.isArray(t.entries))throw TypeError(".authd.PasswdEntries.entries: array expected");e.entries=[];for(let o=0;o<t.entries.length;++o){if("object"!=typeof t.entries[o])throw TypeError(".authd.PasswdEntries.entries: object expected");e.entries[o]=$root.authd.PasswdEntry.fromObject(t.entries[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.entries=[]),t.entries&&t.entries.length){o.entries=[];for(let r=0;r<t.entries.length;++r)o.entries[r]=$root.authd.PasswdEntry.toObject(t.entries[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GroupEntry=function(){function t(t){if(this.members=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.name="",t.prototype.passwd="",t.prototype.gid=0,t.prototype.members=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.authd.GroupEntry)return t;let e=new $root.authd.GroupEntry;if(null!=t.name&&(e.name=String(t.name)),null!=t.passwd&&(e.passwd=String(t.passwd)),null!=t.gid&&(e.gid=t.gid>>>0),t.members){if(!Array.isArray(t.members))throw TypeError(".authd.GroupEntry.members: array expected");e.members=[];for(let o=0;o<t.members.length;++o)e.members[o]=String(t.members[o])}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.members=[]),e.defaults&&(o.name="",o.passwd="",o.gid=0),null!=t.name&&t.hasOwnProperty("name")&&(o.name=t.name),null!=t.passwd&&t.hasOwnProperty("passwd")&&(o.passwd=t.passwd),null!=t.gid&&t.hasOwnProperty("gid")&&(o.gid=t.gid),t.members&&t.members.length){o.members=[];for(let e=0;e<t.members.length;++e)o.members[e]=t.members[e]}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.GroupEntries=function(){function t(t){if(this.entries=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.entries=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.authd.GroupEntries)return t;let e=new $root.authd.GroupEntries;if(t.entries){if(!Array.isArray(t.entries))throw TypeError(".authd.GroupEntries.entries: array expected");e.entries=[];for(let o=0;o<t.entries.length;++o){if("object"!=typeof t.entries[o])throw TypeError(".authd.GroupEntries.entries: object expected");e.entries[o]=$root.authd.GroupEntry.fromObject(t.entries[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.entries=[]),t.entries&&t.entries.length){o.entries=[];for(let r=0;r<t.entries.length;++r)o.entries[r]=$root.authd.GroupEntry.toObject(t.entries[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.ShadowEntry=function(){function t(t){if(t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.name="",t.prototype.passwd="",t.prototype.lastChange=0,t.prototype.changeMinDays=0,t.prototype.changeMaxDays=0,t.prototype.changeWarnDays=0,t.prototype.changeInactiveDays=0,t.prototype.expireDate=0,t.fromObject=function(t){if(t instanceof $root.authd.ShadowEntry)return t;let e=new $root.authd.ShadowEntry;return null!=t.name&&(e.name=String(t.name)),null!=t.passwd&&(e.passwd=String(t.passwd)),null!=t.lastChange&&(e.lastChange=0|t.lastChange),null!=t.changeMinDays&&(e.changeMinDays=0|t.changeMinDays),null!=t.changeMaxDays&&(e.changeMaxDays=0|t.changeMaxDays),null!=t.changeWarnDays&&(e.changeWarnDays=0|t.changeWarnDays),null!=t.changeInactiveDays&&(e.changeInactiveDays=0|t.changeInactiveDays),null!=t.expireDate&&(e.expireDate=0|t.expireDate),e},t.toObject=function(t,e){e||(e={});let o={};return e.defaults&&(o.name="",o.passwd="",o.lastChange=0,o.changeMinDays=0,o.changeMaxDays=0,o.changeWarnDays=0,o.changeInactiveDays=0,o.expireDate=0),null!=t.name&&t.hasOwnProperty("name")&&(o.name=t.name),null!=t.passwd&&t.hasOwnProperty("passwd")&&(o.passwd=t.passwd),null!=t.lastChange&&t.hasOwnProperty("lastChange")&&(o.lastChange=t.lastChange),null!=t.changeMinDays&&t.hasOwnProperty("changeMinDays")&&(o.changeMinDays=t.changeMinDays),null!=t.changeMaxDays&&t.hasOwnProperty("changeMaxDays")&&(o.changeMaxDays=t.changeMaxDays),null!=t.changeWarnDays&&t.hasOwnProperty("changeWarnDays")&&(o.changeWarnDays=t.changeWarnDays),null!=t.changeInactiveDays&&t.hasOwnProperty("changeInactiveDays")&&(o.changeInactiveDays=t.changeInactiveDays),null!=t.expireDate&&t.hasOwnProperty("expireDate")&&(o.expireDate=t.expireDate),o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t.ShadowEntries=function(){function t(t){if(this.entries=[],t)for(let e=Object.keys(t),o=0;o<e.length;++o)null!=t[e[o]]&&(this[e[o]]=t[e[o]])}return t.prototype.entries=$util.emptyArray,t.fromObject=function(t){if(t instanceof $root.authd.ShadowEntries)return t;let e=new $root.authd.ShadowEntries;if(t.entries){if(!Array.isArray(t.entries))throw TypeError(".authd.ShadowEntries.entries: array expected");e.entries=[];for(let o=0;o<t.entries.length;++o){if("object"!=typeof t.entries[o])throw TypeError(".authd.ShadowEntries.entries: object expected");e.entries[o]=$root.authd.ShadowEntry.fromObject(t.entries[o])}}return e},t.toObject=function(t,e){e||(e={});let o={};if((e.arrays||e.defaults)&&(o.entries=[]),t.entries&&t.entries.length){o.entries=[];for(let r=0;r<t.entries.length;++r)o.entries[r]=$root.authd.ShadowEntry.toObject(t.entries[r],e)}return o},t.prototype.toJSON=function(){return this.constructor.toObject(this,minimal.util.toJSONOptions)},t}(),t})(),pam=$root.pam=(()=>{const t={};return t.Stage=function(){const t={},e=Object.create(t);return e[t[0]="userSelection"]=0,e[t[1]="brokerSelection"]=1,e[t[2]="authModeSelection"]=2,e[t[3]="challenge"]=3,e}(),t})();export{authd,$root as default,gdm,pam};
(uuay)overviewControls.js)w// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GLib from 'gi://GLib';
import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as AppDisplay from './appDisplay.js';
import * as Dash from './dash.js';
import * as Layout from './layout.js';
import * as Main from './main.js';
import * as Overview from './overview.js';
import * as SearchController from './searchController.js';
import * as Util from '../misc/util.js';
import * as WindowManager from './windowManager.js';
import * as WorkspaceThumbnail from './workspaceThumbnail.js';
import * as WorkspacesView from './workspacesView.js';

export const SMALL_WORKSPACE_RATIO = 0.15;
const DASH_MAX_HEIGHT_RATIO = 0.15;

const A11Y_SCHEMA = 'org.gnome.desktop.a11y.keyboard';

export const SIDE_CONTROLS_ANIMATION_TIME = 250;

/** @enum {number} */
export const ControlsState = {
    HIDDEN: 0,
    WINDOW_PICKER: 1,
    APP_GRID: 2,
};

const ControlsManagerLayout = GObject.registerClass(
class ControlsManagerLayout extends Clutter.LayoutManager {
    _init(searchEntry, appDisplay, workspacesDisplay, workspacesThumbnails,
        searchController, dash, stateAdjustment) {
        super._init();

        this._appDisplay = appDisplay;
        this._workspacesDisplay = workspacesDisplay;
        this._workspacesThumbnails = workspacesThumbnails;
        this._stateAdjustment = stateAdjustment;
        this._searchEntry = searchEntry;
        this._searchController = searchController;
        this._dash = dash;

        this._spacing = 0;

        this._cachedWorkspaceBoxes = new Map();
        this._postAllocationCallbacks = [];

        stateAdjustment.connect('notify::value', () => this.layout_changed());

        this._workAreaBox = new Clutter.ActorBox();
        global.display.connectObject(
            'workareas-changed', () => this._updateWorkAreaBox(),
            this);
        Main.layoutManager.connectObject(
            'monitors-changed', () => this._updateWorkAreaBox(),
            this);
        this._updateWorkAreaBox();
    }

    _updateWorkAreaBox() {
        const monitor = Main.layoutManager.primaryMonitor;
        if (!monitor)
            return;

        const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor.index);
        const startX = workArea.x - monitor.x;
        const startY = workArea.y - monitor.y;
        this._workAreaBox.set_origin(startX, startY);
        this._workAreaBox.set_size(workArea.width, workArea.height);
    }

    _computeWorkspacesBoxForState(state, box, searchHeight, dashHeight, thumbnailsHeight) {
        const workspaceBox = box.copy();
        const [width, height] = workspaceBox.get_size();
        const {y1: startY} = this._workAreaBox;
        const {expandFraction} = this._workspacesThumbnails;

        switch (state) {
        case ControlsState.HIDDEN:
            workspaceBox.set_origin(...this._workAreaBox.get_origin());
            workspaceBox.set_size(...this._workAreaBox.get_size());
            break;
        case ControlsState.WINDOW_PICKER:
            workspaceBox.set_origin(0,
                startY + searchHeight + this._spacing +
                thumbnailsHeight + this._spacing * expandFraction);
            workspaceBox.set_size(width,
                height -
                dashHeight - this._spacing -
                searchHeight - this._spacing -
                thumbnailsHeight - this._spacing * expandFraction);
            break;
        case ControlsState.APP_GRID:
            workspaceBox.set_origin(0, startY + searchHeight + this._spacing);
            workspaceBox.set_size(
                width,
                Math.round(height * SMALL_WORKSPACE_RATIO));
            break;
        }

        return workspaceBox;
    }

    _getAppDisplayBoxForState(state, box, searchHeight, dashHeight, appGridBox) {
        const [width, height] = box.get_size();
        const {y1: startY} = this._workAreaBox;
        const appDisplayBox = new Clutter.ActorBox();

        switch (state) {
        case ControlsState.HIDDEN:
        case ControlsState.WINDOW_PICKER:
            appDisplayBox.set_origin(0, box.y2);
            break;
        case ControlsState.APP_GRID:
            appDisplayBox.set_origin(0,
                startY + searchHeight + this._spacing + appGridBox.get_height());
            break;
        }

        appDisplayBox.set_size(width,
            height -
            searchHeight - this._spacing -
            appGridBox.get_height() - this._spacing -
            dashHeight);

        return appDisplayBox;
    }

    _runPostAllocation() {
        if (this._postAllocationCallbacks.length === 0)
            return;

        this._postAllocationCallbacks.forEach(cb => cb());
        this._postAllocationCallbacks = [];
    }

    vfunc_set_container(container) {
        this._container?.disconnectObject(this);
        this._container = container;
        this._container?.connectObject('style-changed',
            () => {
                const node = this._container.get_theme_node();
                const spacing = node.get_length('spacing');
                if (this._spacing !== spacing) {
                    this._spacing = spacing;
                    this.layout_changed();
                }
            }, this);
    }

    vfunc_get_preferred_width(_container, _forHeight) {
        // The MonitorConstraint will allocate us a fixed size anyway
        return [0, 0];
    }

    vfunc_get_preferred_height(_container, _forWidth) {
        // The MonitorConstraint will allocate us a fixed size anyway
        return [0, 0];
    }

    vfunc_allocate(container, box) {
        const childBox = new Clutter.ActorBox();

        const startY = this._workAreaBox.y1;
        box.y1 += startY;
        const [width, height] = box.get_size();
        let availableHeight = height;

        // Search entry
        let [searchHeight] = this._searchEntry.get_preferred_height(width);
        childBox.set_origin(0, startY);
        childBox.set_size(width, searchHeight);
        this._searchEntry.allocate(childBox);

        availableHeight -= searchHeight + this._spacing;

        // Dash
        const maxDashHeight = Math.round(box.get_height() * DASH_MAX_HEIGHT_RATIO);
        this._dash.setMaxSize(width, maxDashHeight);

        let [, dashHeight] = this._dash.get_preferred_height(width);
        dashHeight = Math.min(dashHeight, maxDashHeight);
        childBox.set_origin(0, startY + height - dashHeight);
        childBox.set_size(width, dashHeight);
        this._dash.allocate(childBox);

        availableHeight -= dashHeight + this._spacing;

        // Workspace Thumbnails
        let thumbnailsHeight = 0;
        if (this._workspacesThumbnails.visible) {
            const {expandFraction} = this._workspacesThumbnails;
            [thumbnailsHeight] =
                this._workspacesThumbnails.get_preferred_height(width);
            thumbnailsHeight = Math.min(
                thumbnailsHeight * expandFraction,
                height * this._workspacesThumbnails.maxThumbnailScale);
            childBox.set_origin(0, startY + searchHeight + this._spacing);
            childBox.set_size(width, thumbnailsHeight);
            this._workspacesThumbnails.allocate(childBox);
        }

        // Workspaces
        let params = [box, searchHeight, dashHeight, thumbnailsHeight];
        const transitionParams = this._stateAdjustment.getStateTransitionParams();

        // Update cached boxes
        for (const state of Object.values(ControlsState)) {
            this._cachedWorkspaceBoxes.set(
                state, this._computeWorkspacesBoxForState(state, ...params));
        }

        let workspacesBox;
        if (!transitionParams.transitioning) {
            workspacesBox = this._cachedWorkspaceBoxes.get(transitionParams.currentState);
        } else {
            const initialBox = this._cachedWorkspaceBoxes.get(transitionParams.initialState);
            const finalBox = this._cachedWorkspaceBoxes.get(transitionParams.finalState);
            workspacesBox = initialBox.interpolate(finalBox, transitionParams.progress);
        }

        this._workspacesDisplay.allocate(workspacesBox);

        // AppDisplay
        if (this._appDisplay.visible) {
            const workspaceAppGridBox =
                this._cachedWorkspaceBoxes.get(ControlsState.APP_GRID);

            params = [box, searchHeight, dashHeight, workspaceAppGridBox];
            let appDisplayBox;
            if (!transitionParams.transitioning) {
                appDisplayBox =
                    this._getAppDisplayBoxForState(transitionParams.currentState, ...params);
            } else {
                const initialBox =
                    this._getAppDisplayBoxForState(transitionParams.initialState, ...params);
                const finalBox =
                    this._getAppDisplayBoxForState(transitionParams.finalState, ...params);

                appDisplayBox = initialBox.interpolate(finalBox, transitionParams.progress);
            }

            this._appDisplay.allocate(appDisplayBox);
        }

        // Search
        childBox.set_origin(0, startY + searchHeight + this._spacing);
        childBox.set_size(width, availableHeight);

        this._searchController.allocate(childBox);

        this._runPostAllocation();
    }

    ensureAllocation() {
        this.layout_changed();
        return new Promise(
            resolve => this._postAllocationCallbacks.push(resolve));
    }

    getWorkspacesBoxForState(state) {
        return this._cachedWorkspaceBoxes.get(state);
    }
});

export const OverviewAdjustment = GObject.registerClass({
    Properties: {
        'gesture-in-progress': GObject.ParamSpec.boolean(
            'gesture-in-progress', 'Gesture in progress', 'Gesture in progress',
            GObject.ParamFlags.READWRITE,
            false),
    },
}, class OverviewAdjustment extends St.Adjustment {
    _init(actor) {
        super._init({
            actor,
            value: ControlsState.WINDOW_PICKER,
            lower: ControlsState.HIDDEN,
            upper: ControlsState.APP_GRID,
        });
    }

    getStateTransitionParams() {
        const currentState = this.value;

        const transition = this.get_transition('value');
        let initialState = transition
            ? transition.get_interval().peek_initial_value()
            : currentState;
        let finalState = transition
            ? transition.get_interval().peek_final_value()
            : currentState;

        if (initialState > finalState) {
            initialState = Math.ceil(initialState);
            finalState = Math.floor(finalState);
        } else {
            initialState = Math.floor(initialState);
            finalState = Math.ceil(finalState);
        }

        const length = Math.abs(finalState - initialState);
        const progress = length > 0
            ? Math.abs((currentState - initialState) / length)
            : 1;

        return {
            transitioning: transition !== null || this.gestureInProgress,
            currentState,
            initialState,
            finalState,
            progress,
        };
    }
});

export const ControlsManager = GObject.registerClass(
class ControlsManager extends St.Widget {
    _init() {
        super._init({
            style_class: 'controls-manager',
            x_expand: true,
            y_expand: true,
            clip_to_allocation: true,
        });

        this._ignoreShowAppsButtonToggle = false;

        this._searchEntry = new St.Entry({
            style_class: 'search-entry',
            /* Translators: this is the text displayed
               in the search entry when no search is
               active; it should not exceed ~30
               characters. */
            hint_text: _('Type to search'),
            track_hover: true,
            can_focus: true,
        });
        this._searchEntry.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);
        this._searchEntryBin = new St.Bin({
            child: this._searchEntry,
            x_align: Clutter.ActorAlign.CENTER,
        });

        this.dash = new Dash.Dash();

        this._workspaceAdjustment = Main.createWorkspacesAdjustment(this);

        this._stateAdjustment = new OverviewAdjustment(this);
        this._stateAdjustment.connect('notify::value', this._update.bind(this));

        this._searchController = new SearchController.SearchController(
            this._searchEntry,
            this.dash.showAppsButton);
        this._searchController.connect('notify::search-active', this._onSearchChanged.bind(this));

        Main.layoutManager.connect('monitors-changed', () => {
            this._thumbnailsBox.setMonitorIndex(Main.layoutManager.primaryIndex);
        });
        this._thumbnailsBox = new WorkspaceThumbnail.ThumbnailsBox(
            this._workspaceAdjustment, Main.layoutManager.primaryIndex);
        this._thumbnailsBox.connect('notify::should-show', () => {
            this._thumbnailsBox.show();
            this._thumbnailsBox.ease_property('expand-fraction',
                this._thumbnailsBox.should_show ? 1 : 0, {
                    duration: SIDE_CONTROLS_ANIMATION_TIME,
                    mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                    onComplete: () => this._updateThumbnailsBox(),
                });
        });

        this._workspacesDisplay = new WorkspacesView.WorkspacesDisplay(
            this,
            this._workspaceAdjustment,
            this._stateAdjustment);
        this._appDisplay = new AppDisplay.AppDisplay();

        this.add_child(this._searchEntryBin);
        this.add_child(this._appDisplay);
        this.add_child(this.dash);
        this.add_child(this._searchController);
        this.add_child(this._thumbnailsBox);
        this.add_child(this._workspacesDisplay);

        this.layout_manager = new ControlsManagerLayout(
            this._searchEntryBin,
            this._appDisplay,
            this._workspacesDisplay,
            this._thumbnailsBox,
            this._searchController,
            this.dash,
            this._stateAdjustment);

        this.dash.showAppsButton.connect('notify::checked',
            this._onShowAppsButtonToggled.bind(this));

        Main.ctrlAltTabManager.addGroup(
            this.appDisplay,
            _('Apps'),
            'shell-focus-app-grid-symbolic', {
                proxy: this,
                focusCallback: () => {
                    this.dash.showAppsButton.checked = true;
                    this.appDisplay.navigate_focus(
                        null, St.DirectionType.TAB_FORWARD, false);
                },
            });

        Main.ctrlAltTabManager.addGroup(
            this._workspacesDisplay,
            _('Windows'),
            'shell-focus-windows-symbolic', {
                proxy: this,
                focusCallback: () => {
                    this.dash.showAppsButton.checked = false;
                    this._workspacesDisplay.navigate_focus(
                        null, St.DirectionType.TAB_FORWARD, false);
                },
            });

        this._a11ySettings = new Gio.Settings({schema_id: A11Y_SCHEMA});

        this._lastOverlayKeyTime = 0;
        global.display.connect('overlay-key', () => {
            if (this._a11ySettings.get_boolean('stickykeys-enable'))
                return;

            const {initialState, finalState, transitioning} =
                this._stateAdjustment.getStateTransitionParams();

            const time = GLib.get_monotonic_time() / 1000;
            const timeDiff = time - this._lastOverlayKeyTime;
            this._lastOverlayKeyTime = time;

            const shouldShift = St.Settings.get().enable_animations
                ? transitioning && finalState > initialState
                : Main.overview.visible && timeDiff < Overview.ANIMATION_TIME;

            if (shouldShift)
                this._shiftState(Meta.MotionDirection.UP);
            else
                Main.overview.toggle();
        });

        // connect_after to give search controller first dibs on the event
        global.stage.connect_after('key-press-event', (actor, event) => {
            if (this._searchController.searchActive)
                return Clutter.EVENT_PROPAGATE;

            if (global.stage.key_focus &&
                !this.contains(global.stage.key_focus))
                return Clutter.EVENT_PROPAGATE;

            const {finalState} =
                this._stateAdjustment.getStateTransitionParams();
            let keynavDisplay;

            if (finalState === ControlsState.WINDOW_PICKER)
                keynavDisplay = this._workspacesDisplay;
            else if (finalState === ControlsState.APP_GRID)
                keynavDisplay = this._appDisplay;

            if (!keynavDisplay)
                return Clutter.EVENT_PROPAGATE;

            const symbol = event.get_key_symbol();
            if (symbol === Clutter.KEY_Tab || symbol === Clutter.KEY_Down) {
                keynavDisplay.navigate_focus(
                    null, St.DirectionType.TAB_FORWARD, false);
                return Clutter.EVENT_STOP;
            } else if (symbol === Clutter.KEY_ISO_Left_Tab) {
                keynavDisplay.navigate_focus(
                    null, St.DirectionType.TAB_BACKWARD, false);
                return Clutter.EVENT_STOP;
            }

            return Clutter.EVENT_PROPAGATE;
        });

        Main.wm.addKeybinding(
            'toggle-application-view',
            new Gio.Settings({schema_id: WindowManager.SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            this._toggleAppsPage.bind(this));

        Main.wm.addKeybinding('shift-overview-up',
            new Gio.Settings({schema_id: WindowManager.SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            () => this._shiftState(Meta.MotionDirection.UP));

        Main.wm.addKeybinding('shift-overview-down',
            new Gio.Settings({schema_id: WindowManager.SHELL_KEYBINDINGS_SCHEMA}),
            Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
            Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW,
            () => this._shiftState(Meta.MotionDirection.DOWN));

        this._update();

        this.connect('destroy', this._onDestroy.bind(this));
    }

    _getFitModeForState(state) {
        switch (state) {
        case ControlsState.HIDDEN:
        case ControlsState.WINDOW_PICKER:
            return WorkspacesView.FitMode.SINGLE;
        case ControlsState.APP_GRID:
            return WorkspacesView.FitMode.ALL;
        default:
            return WorkspacesView.FitMode.SINGLE;
        }
    }

    _getThumbnailsBoxParams() {
        const {initialState, finalState, progress} =
            this._stateAdjustment.getStateTransitionParams();

        const paramsForState = s => {
            let opacity, scale, translationY;
            switch (s) {
            case ControlsState.HIDDEN:
            case ControlsState.WINDOW_PICKER:
                opacity = 255;
                scale = 1;
                translationY = 0;
                break;
            case ControlsState.APP_GRID:
                opacity = 0;
                scale = 0.5;
                translationY = this._thumbnailsBox.height / 2;
                break;
            default:
                opacity = 255;
                scale = 1;
                translationY = 0;
                break;
            }

            return {opacity, scale, translationY};
        };

        const initialParams = paramsForState(initialState);
        const finalParams = paramsForState(finalState);

        return [
            Util.lerp(initialParams.opacity, finalParams.opacity, progress),
            Util.lerp(initialParams.scale, finalParams.scale, progress),
            Util.lerp(initialParams.translationY, finalParams.translationY, progress),
        ];
    }

    _updateThumbnailsBox(animate = false) {
        const {shouldShow} = this._thumbnailsBox;
        const {searchActive} = this._searchController;
        const [opacity, scale, translationY] = this._getThumbnailsBoxParams();

        const thumbnailsBoxVisible = shouldShow && !searchActive && opacity !== 0;
        if (thumbnailsBoxVisible) {
            this._thumbnailsBox.opacity = 0;
            this._thumbnailsBox.visible = thumbnailsBoxVisible;
        }

        const params = {
            opacity: searchActive ? 0 : opacity,
            duration: animate ? SIDE_CONTROLS_ANIMATION_TIME : 0,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => (this._thumbnailsBox.visible = thumbnailsBoxVisible),
        };

        if (!searchActive) {
            params.scale_x = scale;
            params.scale_y = scale;
            params.translation_y = translationY;
        }

        this._thumbnailsBox.ease(params);
    }

    _updateAppDisplayVisibility(stateTransitionParams = null) {
        if (!stateTransitionParams)
            stateTransitionParams = this._stateAdjustment.getStateTransitionParams();

        const {initialState, finalState} = stateTransitionParams;
        const state = Math.max(initialState, finalState);

        this._appDisplay.visible =
            state > ControlsState.WINDOW_PICKER &&
            !this._searchController.searchActive;
    }

    _update() {
        const params = this._stateAdjustment.getStateTransitionParams();

        const fitMode = Util.lerp(
            this._getFitModeForState(params.initialState),
            this._getFitModeForState(params.finalState),
            params.progress);

        const {fitModeAdjustment} = this._workspacesDisplay;
        fitModeAdjustment.value = fitMode;

        this._updateThumbnailsBox();
        this._updateAppDisplayVisibility(params);
    }

    _onSearchChanged() {
        const {searchActive} = this._searchController;

        if (!searchActive) {
            this._updateAppDisplayVisibility();
            this._workspacesDisplay.reactive = true;
            this._workspacesDisplay.setPrimaryWorkspaceVisible(true);
        } else {
            this._searchController.show();
        }

        this._updateThumbnailsBox(true);

        this._appDisplay.ease({
            opacity: searchActive ? 0 : 255,
            duration: SIDE_CONTROLS_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => this._updateAppDisplayVisibility(),
        });
        this._workspacesDisplay.ease({
            opacity: searchActive ? 0 : 255,
            duration: SIDE_CONTROLS_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => {
                this._workspacesDisplay.reactive = !searchActive;
                this._workspacesDisplay.setPrimaryWorkspaceVisible(!searchActive);
            },
        });
        this._searchController.ease({
            opacity: searchActive ? 255 : 0,
            duration: SIDE_CONTROLS_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => (this._searchController.visible = searchActive),
        });
    }

    _onShowAppsButtonToggled() {
        if (this._ignoreShowAppsButtonToggle)
            return;

        const checked = this.dash.showAppsButton.checked;

        const value = checked
            ? ControlsState.APP_GRID : ControlsState.WINDOW_PICKER;
        this._stateAdjustment.remove_transition('value');
        this._stateAdjustment.ease(value, {
            duration: SIDE_CONTROLS_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });
    }

    _toggleAppsPage() {
        if (Main.overview.visible) {
            const checked = this.dash.showAppsButton.checked;
            this.dash.showAppsButton.checked = !checked;
        } else {
            Main.overview.show(ControlsState.APP_GRID);
        }
    }

    _shiftState(direction) {
        let {currentState, finalState} = this._stateAdjustment.getStateTransitionParams();

        if (direction === Meta.MotionDirection.DOWN)
            finalState = Math.max(finalState - 1, ControlsState.HIDDEN);
        else if (direction === Meta.MotionDirection.UP)
            finalState = Math.min(finalState + 1, ControlsState.APP_GRID);

        if (finalState === currentState)
            return;

        if (currentState === ControlsState.HIDDEN &&
            finalState === ControlsState.WINDOW_PICKER) {
            Main.overview.show();
        } else if (finalState === ControlsState.HIDDEN) {
            Main.overview.hide();
        } else {
            this._stateAdjustment.ease(finalState, {
                duration: SIDE_CONTROLS_ANIMATION_TIME,
                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
                onComplete: () => {
                    this.dash.showAppsButton.checked =
                        finalState === ControlsState.APP_GRID;
                },
            });
        }
    }

    vfunc_unmap() {
        super.vfunc_unmap();
        this._workspacesDisplay?.hide();
    }

    _onDestroy() {
        delete this._searchEntryBin;
        delete this._appDisplay;
        delete this.dash;
        delete this._searchController;
        delete this._thumbnailsBox;
        delete this._workspacesDisplay;
    }

    prepareToEnterOverview() {
        this._searchController.prepareToEnterOverview();
        this._workspacesDisplay.prepareToEnterOverview();
    }

    prepareToLeaveOverview() {
        this._searchController.prepareToLeaveOverview();
        this._workspacesDisplay.prepareToLeaveOverview();
    }

    animateToOverview(state, callback) {
        this._ignoreShowAppsButtonToggle = true;

        this._stateAdjustment.value = ControlsState.HIDDEN;
        this._stateAdjustment.ease(state, {
            duration: Overview.ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onStopped: () => {
                if (callback)
                    callback();
            },
        });

        this.dash.showAppsButton.checked =
            state === ControlsState.APP_GRID;

        this._ignoreShowAppsButtonToggle = false;
    }

    animateFromOverview(callback) {
        this._ignoreShowAppsButtonToggle = true;

        this._stateAdjustment.ease(ControlsState.HIDDEN, {
            duration: Overview.ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onStopped: () => {
                this.dash.showAppsButton.checked = false;
                this._ignoreShowAppsButtonToggle = false;

                if (callback)
                    callback();
            },
        });
    }

    getWorkspacesBoxForState(state) {
        return this.layoutManager.getWorkspacesBoxForState(state);
    }

    gestureBegin(tracker) {
        const baseDistance = global.screen_height;
        const progress = this._stateAdjustment.value;
        const points = [
            ControlsState.HIDDEN,
            ControlsState.WINDOW_PICKER,
            ControlsState.APP_GRID,
        ];

        const transition = this._stateAdjustment.get_transition('value');
        const cancelProgress = transition
            ? transition.get_interval().peek_final_value()
            : Math.round(progress);
        this._stateAdjustment.remove_transition('value');

        tracker.confirmSwipe(baseDistance, points, progress, cancelProgress);
        this.prepareToEnterOverview();
        this._stateAdjustment.gestureInProgress = true;
    }

    gestureProgress(progress) {
        this._stateAdjustment.value = progress;
    }

    gestureEnd(target, duration, onComplete) {
        if (target === ControlsState.HIDDEN)
            this.prepareToLeaveOverview();

        this.dash.showAppsButton.checked =
            target === ControlsState.APP_GRID;

        this._stateAdjustment.remove_transition('value');
        this._stateAdjustment.ease(target, {
            duration,
            mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
            onComplete,
        });

        this._stateAdjustment.gestureInProgress = false;
    }

    async runStartupAnimation(callback) {
        this._ignoreShowAppsButtonToggle = true;

        this.prepareToEnterOverview();

        this._stateAdjustment.value = ControlsState.HIDDEN;
        this._stateAdjustment.ease(ControlsState.WINDOW_PICKER, {
            duration: Overview.ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        this.dash.showAppsButton.checked = false;
        this._ignoreShowAppsButtonToggle = false;

        // Set the opacity here to avoid a 1-frame flicker
        this.opacity = 0;

        // We can't run the animation before the first allocation happens
        await this.layout_manager.ensureAllocation();

        const {STARTUP_ANIMATION_TIME} = Layout;

        // Opacity
        this.ease({
            opacity: 255,
            duration: STARTUP_ANIMATION_TIME,
            mode: Clutter.AnimationMode.LINEAR,
        });

        // Search bar falls from the ceiling
        const {primaryMonitor} = Main.layoutManager;
        const [, y] = this._searchEntryBin.get_transformed_position();
        const yOffset = y - primaryMonitor.y;

        this._searchEntryBin.translation_y = -(yOffset + this._searchEntryBin.height);
        this._searchEntryBin.ease({
            translation_y: 0,
            duration: STARTUP_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
        });

        // The Dash rises from the bottom. This is the last animation to finish,
        // so run the callback there.
        this.dash.translation_y = this.dash.height + this.dash.margin_bottom;
        this.dash.ease({
            translation_y: 0,
            delay: STARTUP_ANIMATION_TIME,
            duration: STARTUP_ANIMATION_TIME,
            mode: Clutter.AnimationMode.EASE_OUT_QUAD,
            onComplete: () => callback(),
        });
    }

    get searchController() {
        return this._searchController;
    }

    get searchEntry() {
        return this._searchEntry;
    }

    get appDisplay() {
        return this._appDisplay;
    }
});
(uuay)keyboardManager.jsC// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import GLib from 'gi://GLib';
import GnomeDesktop from 'gi://GnomeDesktop';

import * as Main from '../ui/main.js';

export const DEFAULT_LOCALE = 'en_US';
export const DEFAULT_LAYOUT = 'us';
export const DEFAULT_VARIANT = '';

let _xkbInfo = null;

/**
 * @returns {GnomeDesktop.XkbInfo}
 */
export function getXkbInfo() {
    if (_xkbInfo == null)
        _xkbInfo = new GnomeDesktop.XkbInfo();
    return _xkbInfo;
}

let _keyboardManager = null;

/**
 * @returns {KeyboardManager}
 */
export function getKeyboardManager() {
    if (_keyboardManager == null)
        _keyboardManager = new KeyboardManager();
    return _keyboardManager;
}

export function releaseKeyboard() {
    if (Main.modalCount > 0)
        global.backend.unfreeze_keyboard(global.get_current_time());
    else
        global.backend.ungrab_keyboard(global.get_current_time());
}

export function holdKeyboard() {
    global.backend.freeze_keyboard(global.get_current_time());
}

class KeyboardManager {
    constructor() {
        // The XKB protocol doesn't allow for more that 4 layouts in a
        // keymap. Wayland doesn't impose this limit and libxkbcommon can
        // handle up to 32 layouts but since we need to support X clients
        // even as a Wayland compositor, we can't bump this.
        this.MAX_LAYOUTS_PER_GROUP = 4;

        this._xkbInfo = getXkbInfo();
        this._current = null;
        this._localeLayoutInfo = this._getLocaleLayout();
        this._layoutInfos = {};
        this._currentKeymap = null;
    }

    _applyLayoutGroup(group) {
        let options = this._buildOptionsString();
        let [layouts, variants] = this._buildGroupStrings(group);
        let model = this._xkbModel;

        if (this._currentKeymap &&
            this._currentKeymap.layouts === layouts &&
            this._currentKeymap.variants === variants &&
            this._currentKeymap.options === options &&
            this._currentKeymap.model === model)
            return;

        this._currentKeymap = {layouts, variants, options, model};
        global.backend.set_keymap(layouts, variants, options, model);
    }

    _applyLayoutGroupIndex(idx) {
        global.backend.lock_layout_group(idx);
    }

    apply(id) {
        let info = this._layoutInfos[id];
        if (!info)
            return;

        if (this._current && this._current.group === info.group) {
            if (this._current.groupIndex !== info.groupIndex)
                this._applyLayoutGroupIndex(info.groupIndex);
        } else {
            this._applyLayoutGroup(info.group);
            this._applyLayoutGroupIndex(info.groupIndex);
        }

        this._current = info;
    }

    reapply() {
        if (!this._current)
            return;

        this._applyLayoutGroup(this._current.group);
        this._applyLayoutGroupIndex(this._current.groupIndex);
    }

    setUserLayouts(ids) {
        this._current = null;
        this._layoutInfos = {};

        for (let i = 0; i < ids.length; ++i) {
            let [found, , , _layout, _variant] = this._xkbInfo.get_layout_info(ids[i]);
            if (found)
                this._layoutInfos[ids[i]] = {id: ids[i], layout: _layout, variant: _variant};
        }

        let i = 0;
        let group = [];
        for (let id in this._layoutInfos) {
            // We need to leave one slot on each group free so that we
            // can add a layout containing the symbols for the
            // language used in UI strings to ensure that toolkits can
            // handle mnemonics like Alt+Ф even if the user is
            // actually typing in a different layout.
            let groupIndex = i % (this.MAX_LAYOUTS_PER_GROUP - 1);
            if (groupIndex === 0)
                group = [];

            let info = this._layoutInfos[id];
            group[groupIndex] = info;
            info.group = group;
            info.groupIndex = groupIndex;

            i += 1;
        }
    }

    _getLocaleLayout() {
        let locale = GLib.get_language_names()[0];
        if (!locale.includes('_'))
            locale = DEFAULT_LOCALE;

        let [found, , id] = GnomeDesktop.get_input_source_from_locale(locale);
        if (!found)
            [, , id] = GnomeDesktop.get_input_source_from_locale(DEFAULT_LOCALE);

        let _layout, _variant;
        [found, , , _layout, _variant] = this._xkbInfo.get_layout_info(id);
        if (found)
            return {layout: _layout, variant: _variant};
        else
            return {layout: DEFAULT_LAYOUT, variant: DEFAULT_VARIANT};
    }

    _buildGroupStrings(_group) {
        let group = _group.concat(this._localeLayoutInfo);
        let layouts = group.map(g => g.layout).join(',');
        let variants = group.map(g => g.variant).join(',');
        return [layouts, variants];
    }

    setKeyboardOptions(options) {
        this._xkbOptions = options;
    }

    setKeyboardModel(model) {
        this._xkbModel = model;
    }

    _buildOptionsString() {
        let options = this._xkbOptions.join(',');
        return options;
    }

    get currentLayout() {
        return this._current;
    }
}
(uuay)camera.jstimport GObject from 'gi://GObject';
import Shell from 'gi://Shell';

import {SystemIndicator} from '../quickSettings.js';

export const Indicator = GObject.registerClass(
class Indicator extends SystemIndicator {
    constructor() {
        super();

        this._indicator = this._addIndicator();
        this._indicator.icon_name = 'camera-web-symbolic';
        this._indicator.add_style_class_name('privacy-indicator');

        this._cameraMonitor = new Shell.CameraMonitor();
        this._cameraMonitor.bind_property('cameras-in-use', this._indicator,
            'visible', GObject.BindingFlags.SYNC_CREATE);
    }
});
(uuay)dbusErrors.js5import GLib from 'gi://GLib';
import Gio from 'gi://Gio';

function camelcase(str) {
    const words = str.toLowerCase().split('_');
    return words.map(w => `${w.at(0).toUpperCase()}${w.substring(1)}`).join('');
}

function decamelcase(str) {
    return str.replace(/(.)([A-Z])/g, '$1-$2');
}

function registerErrorDomain(domain, errorEnum, prefix = 'org.gnome.Shell') {
    const domainName =
        `shell-${decamelcase(domain).toLowerCase()}-error`;
    const quark = GLib.quark_from_string(domainName);

    for (const [name, code] of Object.entries(errorEnum)) {
        Gio.dbus_error_register_error(quark,
            code, `${prefix}.${domain}.Error.${camelcase(name)}`);
    }
    return quark;
}

export const ModalDialogError = {
    UNKNOWN_TYPE: 0,
    GRAB_FAILED: 1,
};
export const ModalDialogErrors =
    registerErrorDomain('ModalDialog', ModalDialogError);

export const NotificationError = {
    INVALID_APP: 0,
};
export const NotificationErrors =
    registerErrorDomain('Notifications', NotificationError, 'org.gtk');

export const ExtensionError = {
    INFO_DOWNLOAD_FAILED: 0,
    DOWNLOAD_FAILED: 1,
    EXTRACT_FAILED: 2,
    ENABLE_FAILED: 3,
    NOT_ALLOWED: 4,
};
export const ExtensionErrors =
    registerErrorDomain('Extensions', ExtensionError);

export const ScreencastError = {
    ALL_PIPELINES_FAILED: 0,
    PIPELINE_ERROR: 1,
    SAVE_TO_DISK_DISABLED: 2,
    ALREADY_RECORDING: 3,
    RECORDER_ERROR: 4,
    SERVICE_CRASH: 5,
    OUT_OF_DISK_SPACE: 6,
};
export const ScreencastErrors =
    registerErrorDomain('Screencast', ScreencastError);
(uuay)closeDialog.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Dialog from './dialog.js';
import * as Main from './main.js';

const FROZEN_WINDOW_BRIGHTNESS = -0.3;
const DIALOG_TRANSITION_TIME = 150;
const ALIVE_TIMEOUT = 5000;

export const CloseDialog = GObject.registerClass({
    Implements: [Meta.CloseDialog],
    Properties: {
        'window': GObject.ParamSpec.override('window', Meta.CloseDialog),
    },
}, class CloseDialog extends GObject.Object {
    _init(window) {
        super._init();
        this._window = window;
        this._dialog = null;
        this._tracked = undefined;
        this._timeoutId = 0;
    }

    get window() {
        return this._window;
    }

    set window(window) {
        this._window = window;
    }

    _createDialogContent() {
        let tracker = Shell.WindowTracker.get_default();
        let windowApp = tracker.get_window_app(this._window);

        /* Translators: %s is an application name */
        let title = _('“%s” is not responding.').format(windowApp.get_name());
        let description = _('You may choose to wait a short while for it to ' +
                            'continue or force the app to quit entirely.');
        return new Dialog.MessageDialogContent({title, description});
    }

    _updateScale() {
        // Since this is a child of MetaWindowActor (which, for Wayland clients,
        // applies the geometry scale factor to its children itself, see
        // meta_window_actor_set_geometry_scale()), make sure we don't apply
        // the factor twice in the end.
        if (this._window.get_client_type() !== Meta.WindowClientType.WAYLAND)
            return;

        let {scaleFactor} = St.ThemeContext.get_for_stage(global.stage);
        this._dialog.set_scale(1 / scaleFactor, 1 / scaleFactor);
    }

    _initDialog() {
        if (this._dialog)
            return;

        let windowActor = this._window.get_compositor_private();
        this._dialog = new Dialog.Dialog(windowActor, 'close-dialog');
        this._dialog.width = windowActor.width;
        this._dialog.height = windowActor.height;

        this._dialog.contentLayout.add_child(this._createDialogContent());
        this._dialog.addButton({
            label: _('Force Quit'),
            action: this._onClose.bind(this),
            default: true,
        });
        this._dialog.addButton({
            label: _('Wait'),
            action: this._onWait.bind(this),
            key: Clutter.KEY_Escape,
        });

        global.focus_manager.add_group(this._dialog);

        let themeContext = St.ThemeContext.get_for_stage(global.stage);
        themeContext.connect('notify::scale-factor', this._updateScale.bind(this));

        this._updateScale();
    }

    _addWindowEffect() {
        // We set the effect on the surface actor, so the dialog itself
        // (which is a child of the MetaWindowActor) does not get the
        // effect applied itself.
        let windowActor = this._window.get_compositor_private();
        let surfaceActor = windowActor.get_first_child();
        let effect = new Clutter.BrightnessContrastEffect();
        effect.set_brightness(FROZEN_WINDOW_BRIGHTNESS);
        surfaceActor.add_effect_with_name('gnome-shell-frozen-window', effect);
    }

    _removeWindowEffect() {
        let windowActor = this._window.get_compositor_private();
        let surfaceActor = windowActor.get_first_child();
        surfaceActor.remove_effect_by_name('gnome-shell-frozen-window');
    }

    _onWait() {
        this.response(Meta.CloseDialogResponse.WAIT);
    }

    _onClose() {
        this.response(Meta.CloseDialogResponse.FORCE_CLOSE);
    }

    _onFocusChanged() {
        if (Meta.is_wayland_compositor())
            return;

        let focusWindow = global.display.focus_window;
        let keyFocus = global.stage.key_focus;

        let shouldTrack;
        if (focusWindow != null)
            shouldTrack = focusWindow === this._window;
        else
            shouldTrack = keyFocus && this._dialog.contains(keyFocus);

        if (this._tracked === shouldTrack)
            return;

        if (shouldTrack) {
            Main.layoutManager.trackChrome(this._dialog,
                {affectsInputRegion: true});
        } else {
            Main.layoutManager.untrackChrome(this._dialog);
        }

        // The buttons are broken when they aren't added to the input region,
        // so disable them properly in that case
        this._dialog.buttonLayout.get_children().forEach(b => {
            b.reactive = shouldTrack;
        });

        this._tracked = shouldTrack;
    }

    vfunc_show() {
        if (this._dialog != null)
            return;

        Meta.disable_unredirect_for_display(global.display);

        this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, ALIVE_TIMEOUT,
            () => {
                this._window.check_alive(global.display.get_current_time_roundtrip());
                return GLib.SOURCE_CONTINUE;
            });

        global.display.connectObject(
            'notify::focus-window', this._onFocusChanged.bind(this), this);

        global.stage.connectObject(
            'notify::key-focus', this._onFocusChanged.bind(this), this);

        this._addWindowEffect();
        this._initDialog();

        global.connectObject(
            'shutdown', () => this._onWait(), this._dialog);

        this._dialog._dialog.scale_y = 0;
        this._dialog._dialog.set_pivot_point(0.5, 0.5);

        this._dialog._dialog.ease({
            scale_y: 1,
            mode: Clutter.AnimationMode.LINEAR,
            duration: DIALOG_TRANSITION_TIME,
            onComplete: this._onFocusChanged.bind(this),
        });
    }

    vfunc_hide() {
        if (this._dialog == null)
            return;

        Meta.enable_unredirect_for_display(global.display);

        GLib.source_remove(this._timeoutId);
        this._timeoutId = 0;

        global.display.disconnectObject(this);
        global.stage.disconnectObject(this);

        this._dialog._dialog.remove_all_transitions();

        let dialog = this._dialog;
        this._dialog = null;
        this._removeWindowEffect();

        dialog.makeInactive();
        dialog._dialog.ease({
            scale_y: 0,
            mode: Clutter.AnimationMode.LINEAR,
            duration: DIALOG_TRANSITION_TIME,
            onComplete: () => dialog.destroy(),
        });
    }

    vfunc_focus() {
        if (!this._dialog)
            return;

        const keyFocus = global.stage.key_focus;
        if (!keyFocus || !this._dialog.contains(keyFocus))
            this._dialog.initialKeyFocus.grab_key_focus();
    }
});
(uuay)search.js�u// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as AppDisplay from './appDisplay.js';
import * as IconGrid from './iconGrid.js';
import * as Main from './main.js';
import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
import * as RemoteSearch from './remoteSearch.js';
import {ensureActorVisibleInScrollView} from '../misc/animationUtils.js';

import {Highlighter} from '../misc/util.js';

const SEARCH_PROVIDERS_SCHEMA = 'org.gnome.desktop.search-providers';

const MAX_LIST_SEARCH_RESULTS_ROWS = 5;

const MaxWidthBox = GObject.registerClass(
class MaxWidthBox extends St.BoxLayout {
    vfunc_allocate(box) {
        let themeNode = this.get_theme_node();
        let maxWidth = themeNode.get_max_width();
        let availWidth = box.x2 - box.x1;
        let adjustedBox = box;

        if (availWidth > maxWidth) {
            let excessWidth = availWidth - maxWidth;
            adjustedBox.x1 += Math.floor(excessWidth / 2);
            adjustedBox.x2 -= Math.floor(excessWidth / 2);
        }

        super.vfunc_allocate(adjustedBox);
    }
});

export const SearchResult = GObject.registerClass(
class SearchResult extends St.Button {
    _init(provider, metaInfo, resultsView) {
        this.provider = provider;
        this.metaInfo = metaInfo;
        this._resultsView = resultsView;

        super._init({
            reactive: true,
            can_focus: true,
            track_hover: true,
        });
    }

    vfunc_clicked() {
        this.activate();
    }

    activate() {
        this.provider.activateResult(this.metaInfo.id, this._resultsView.terms);

        if (this.metaInfo.clipboardText) {
            St.Clipboard.get_default().set_text(
                St.ClipboardType.CLIPBOARD, this.metaInfo.clipboardText);
        }
        Main.overview.toggle();
    }
});

export const ListSearchResult = GObject.registerClass(
class ListSearchResult extends SearchResult {
    _init(provider, metaInfo, resultsView) {
        super._init(provider, metaInfo, resultsView);

        this.style_class = 'list-search-result';

        let content = new St.BoxLayout({
            style_class: 'list-search-result-content',
            vertical: false,
            x_align: Clutter.ActorAlign.START,
            x_expand: true,
            y_expand: true,
        });
        this.set_child(content);

        let titleBox = new St.BoxLayout({
            style_class: 'list-search-result-title',
            y_align: Clutter.ActorAlign.CENTER,
        });

        content.add_child(titleBox);

        // An icon for, or thumbnail of, content
        let icon = this.metaInfo['createIcon'](this.ICON_SIZE);
        if (icon)
            titleBox.add_child(icon);

        let title = new St.Label({
            text: this.metaInfo['name'],
            y_align: Clutter.ActorAlign.CENTER,
        });
        titleBox.add_child(title);

        this.label_actor = title;

        if (this.metaInfo['description']) {
            this._descriptionLabel = new St.Label({
                style_class: 'list-search-result-description',
                y_align: Clutter.ActorAlign.CENTER,
            });
            content.add_child(this._descriptionLabel);

            this._resultsView.connectObject(
                'terms-changed', this._highlightTerms.bind(this), this);

            this._highlightTerms();
        }
    }

    get ICON_SIZE() {
        return 24;
    }

    _highlightTerms() {
        let markup = this._resultsView.highlightTerms(this.metaInfo['description'].split('\n')[0]);
        this._descriptionLabel.clutter_text.set_markup(markup);
    }
});

export const GridSearchResult = GObject.registerClass(
class GridSearchResult extends SearchResult {
    _init(provider, metaInfo, resultsView) {
        super._init(provider, metaInfo, resultsView);

        this.style_class = 'grid-search-result';

        this.icon = new IconGrid.BaseIcon(this.metaInfo['name'], {
            createIcon: this.metaInfo['createIcon'],
        });
        let content = new St.Bin({
            child: this.icon,
            x_align: Clutter.ActorAlign.START,
            x_expand: true,
            y_expand: true,
        });
        this.set_child(content);
        this.label_actor = this.icon.label;
    }
});

const SearchResultsBase = GObject.registerClass({
    GTypeFlags: GObject.TypeFlags.ABSTRACT,
    Properties: {
        'focus-child': GObject.ParamSpec.object(
            'focus-child', 'focus-child', 'focus-child',
            GObject.ParamFlags.READABLE,
            Clutter.Actor.$gtype),
    },
}, class SearchResultsBase extends St.BoxLayout {
    _init(provider, resultsView) {
        super._init({style_class: 'search-section', vertical: true});

        this.provider = provider;
        this._resultsView = resultsView;

        this._terms = [];
        this._focusChild = null;

        this._resultDisplayBin = new St.Bin();
        this.add_child(this._resultDisplayBin);

        let separator = new St.Widget({style_class: 'search-section-separator'});
        this.add_child(separator);

        this._resultDisplays = {};

        this._cancellable = new Gio.Cancellable();

        this.connect('destroy', this._onDestroy.bind(this));
    }

    _onDestroy() {
        this._terms = [];
    }

    _createResultDisplay(meta) {
        if (this.provider.createResultObject)
            return this.provider.createResultObject(meta);

        return null;
    }

    clear() {
        this._cancellable.cancel();
        for (let resultId in this._resultDisplays)
            this._resultDisplays[resultId].destroy();
        this._resultDisplays = {};
        this._clearResultDisplay();
        this.hide();
    }

    get focusChild() {
        return this._focusChild;
    }

    _keyFocusIn(actor) {
        if (this._focusChild === actor)
            return;
        this._focusChild = actor;
        this.notify('focus-child');
    }

    _setMoreCount(_count) {
    }

    async _ensureResultActors(results) {
        let metasNeeded = results.filter(
            resultId => this._resultDisplays[resultId] === undefined);

        if (metasNeeded.length === 0)
            return;

        this._cancellable.cancel();
        this._cancellable.reset();

        this.provider.resultsMetasInProgress = true;
        const metas = await this.provider.getResultMetas(metasNeeded, this._cancellable);
        this.provider.resultsMetasInProgress = this._cancellable.is_cancelled();

        if (this._cancellable.is_cancelled()) {
            if (metas.length > 0)
                throw new Error(`Search provider ${this.provider.id} returned results after the request was canceled`);
        }

        if (metas.length !== metasNeeded.length) {
            throw new Error(`Wrong number of result metas returned by search provider ${this.provider.id}: ` +
                `expected ${metasNeeded.length} but got ${metas.length}`);
        }

        if (metas.some(meta => !meta.name || !meta.id))
            throw new Error(`Invalid result meta returned from search provider ${this.provider.id}`);

        metasNeeded.forEach((resultId, i) => {
            let meta = metas[i];
            let display = this._createResultDisplay(meta);
            display.connect('key-focus-in', this._keyFocusIn.bind(this));
            this._resultDisplays[resultId] = display;
        });
    }

    async updateSearch(providerResults, terms, callback) {
        this._terms = terms;
        if (providerResults.length === 0) {
            this._clearResultDisplay();
            this.hide();
            callback();
        } else {
            let maxResults = this._getMaxDisplayedResults();
            let results = maxResults > -1
                ? this.provider.filterResults(providerResults, maxResults)
                : providerResults;
            let moreCount = Math.max(providerResults.length - results.length, 0);

            try {
                await this._ensureResultActors(results);

                // To avoid CSS transitions causing flickering when
                // the first search result stays the same, we hide the
                // content while filling in the results.
                this.hide();
                this._clearResultDisplay();
                results.forEach(
                    resultId => this._addItem(this._resultDisplays[resultId]));
                this._setMoreCount(this.provider.canLaunchSearch ? moreCount : 0);
                this.show();
                callback();
            } catch (e) {
                this._clearResultDisplay();
                callback();
            }
        }
    }
});

export const ListSearchResults = GObject.registerClass(
class ListSearchResults extends SearchResultsBase {
    _init(provider, resultsView) {
        super._init(provider, resultsView);

        this._container = new St.BoxLayout({
            style_class: 'search-section-content',
            x_expand: true,
        });
        this.providerInfo = new ProviderInfo(provider);
        this.providerInfo.connect('key-focus-in', this._keyFocusIn.bind(this));
        this.providerInfo.connect('clicked', () => {
            this.providerInfo.animateLaunch();
            provider.launchSearch(this._terms);
            Main.overview.toggle();
        });

        this._container.add_child(this.providerInfo);

        this._content = new St.BoxLayout({
            style_class: 'list-search-results',
            vertical: true,
            x_expand: true,
        });
        this._container.add_child(this._content);

        this._resultDisplayBin.child = this._container;
    }

    _setMoreCount(count) {
        this.providerInfo.setMoreCount(count);
    }

    _getMaxDisplayedResults() {
        return MAX_LIST_SEARCH_RESULTS_ROWS;
    }

    _clearResultDisplay() {
        this._content.remove_all_children();
    }

    _createResultDisplay(meta) {
        return super._createResultDisplay(meta) ||
               new ListSearchResult(this.provider, meta, this._resultsView);
    }

    _addItem(display) {
        this._content.add_child(display);
    }

    getFirstResult() {
        if (this._content.get_n_children() > 0)
            return this._content.get_child_at_index(0);
        else
            return null;
    }
});

const GridSearchResultsLayout = GObject.registerClass({
    Properties: {
        'spacing': GObject.ParamSpec.int('spacing', 'Spacing', 'Spacing',
            GObject.ParamFlags.READWRITE, 0, GLib.MAXINT32, 0),
    },
}, class GridSearchResultsLayout extends Clutter.LayoutManager {
    _init() {
        super._init();
        this._spacing = 0;
    }

    vfunc_set_container(container) {
        this._container = container;
    }

    vfunc_get_preferred_width(container, forHeight) {
        let minWidth = 0;
        let natWidth = 0;
        let first = true;

        for (let child of container) {
            if (!child.visible)
                continue;

            const [childMinWidth, childNatWidth] = child.get_preferred_width(forHeight);

            minWidth = Math.max(minWidth, childMinWidth);
            natWidth += childNatWidth;

            if (first)
                first = false;
            else
                natWidth += this._spacing;
        }

        return [minWidth, natWidth];
    }

    vfunc_get_preferred_height(container, forWidth) {
        let minHeight = 0;
        let natHeight = 0;

        for (let child of container) {
            if (!child.visible)
                continue;

            const [childMinHeight, childNatHeight] = child.get_preferred_height(forWidth);

            minHeight = Math.max(minHeight, childMinHeight);
            natHeight = Math.max(natHeight, childNatHeight);
        }

        return [minHeight, natHeight];
    }

    vfunc_allocate(container, box) {
        const width = box.get_width();

        const childBox = new Clutter.ActorBox();
        childBox.x1 = 0;
        childBox.y1 = 0;

        let first = true;
        for (let child of container) {
            if (!child.visible)
                continue;

            if (first)
                first = false;
            else
                childBox.x1 += this._spacing;

            const [childWidth] = child.get_preferred_width(-1);
            const [childHeight] = child.get_preferred_height(-1);

            if (childBox.x1 + childWidth <= width)
                childBox.set_size(childWidth, childHeight);
            else
                childBox.set_size(0, 0);

            child.allocate(childBox);
            child.can_focus = childBox.get_area() > 0;

            childBox.x1 += childWidth;
        }
    }

    columnsForWidth(width) {
        if (!this._container)
            return -1;

        const [minWidth] = this.get_preferred_width(this._container, -1);

        if (minWidth === 0)
            return -1;

        let nCols = 0;
        while (width > minWidth) {
            width -= minWidth;
            if (nCols > 0)
                width -= this._spacing;
            nCols++;
        }

        return nCols;
    }

    get spacing() {
        return this._spacing;
    }

    set spacing(v) {
        if (this._spacing === v)
            return;
        this._spacing = v;
        this.layout_changed();
    }
});

export const GridSearchResults = GObject.registerClass(
class GridSearchResults extends SearchResultsBase {
    _init(provider, resultsView) {
        super._init(provider, resultsView);

        this._grid = new St.Widget({style_class: 'grid-search-results'});
        this._grid.layout_manager = new GridSearchResultsLayout();

        this._grid.connect('style-changed', () => {
            const node = this._grid.get_theme_node();
            this._grid.layout_manager.spacing = node.get_length('spacing');
        });

        this._resultDisplayBin.child = new St.Bin({
            child: this._grid,
            x_align: Clutter.ActorAlign.CENTER,
        });

        this._maxResults = provider.maxResults ?? -1;
    }

    _onDestroy() {
        if (this._updateSearchLater) {
            const laters = global.compositor.get_laters();
            laters.remove(this._updateSearchLater);
            delete this._updateSearchLater;
        }

        super._onDestroy();
    }

    updateSearch(...args) {
        if (this._notifyAllocationId)
            this.disconnect(this._notifyAllocationId);
        if (this._updateSearchLater) {
            const laters = global.compositor.get_laters();
            laters.remove(this._updateSearchLater);
            delete this._updateSearchLater;
        }

        // Make sure the maximum number of results calculated by
        // _getMaxDisplayedResults() is updated after width changes.
        this._notifyAllocationId = this.connect('notify::allocation', () => {
            if (this._updateSearchLater)
                return;
            const laters = global.compositor.get_laters();
            this._updateSearchLater = laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
                delete this._updateSearchLater;
                super.updateSearch(...args);
                return GLib.SOURCE_REMOVE;
            });
        });

        super.updateSearch(...args);
    }

    _getMaxDisplayedResults() {
        const width = this.allocation.get_width();
        if (width === 0)
            return this._maxResults;

        const nCols = this._grid.layout_manager.columnsForWidth(width);
        if (nCols < 0)
            return this._maxResults;

        if (this._maxResults < 0)
            return nCols;

        return Math.min(nCols, this._maxResults);
    }

    _clearResultDisplay() {
        this._grid.remove_all_children();
    }

    _createResultDisplay(meta) {
        return super._createResultDisplay(meta) ||
               new GridSearchResult(this.provider, meta, this._resultsView);
    }

    _addItem(display) {
        this._grid.add_child(display);
    }

    getFirstResult() {
        for (let child of this._grid) {
            if (child.visible)
                return child;
        }
        return null;
    }
});

export const SearchResultsView = GObject.registerClass({
    Signals: {'terms-changed': {}},
}, class SearchResultsView extends St.BoxLayout {
    _init() {
        super._init({
            name: 'searchResults',
            vertical: true,
            x_expand: true,
            y_expand: true,
        });

        this._parentalControlsManager = ParentalControlsManager.getDefault();
        this._parentalControlsManager.connect('app-filter-changed', this._reloadRemoteProviders.bind(this));

        this._content = new MaxWidthBox({
            name: 'searchResultsContent',
            vertical: true,
            x_expand: true,
        });

        this._scrollView = new St.ScrollView({
            overlay_scrollbars: true,
            style_class: 'search-display vfade',
            x_expand: true,
            y_expand: true,
            child: this._content,
        });

        let action = new Clutter.PanAction({interpolate: true});
        action.connect('pan', this._onPan.bind(this));
        this._scrollView.add_action(action);

        this.add_child(this._scrollView);

        this._statusText = new St.Label({
            style_class: 'search-statustext',
            x_align: Clutter.ActorAlign.CENTER,
            y_align: Clutter.ActorAlign.CENTER,
        });
        this._statusBin = new St.Bin({
            y_expand: true,
            child: this._statusText,
        });
        this.add_child(this._statusBin);

        this._highlightDefault = false;
        this._defaultResult = null;
        this._startingSearch = false;

        this._terms = [];
        this._results = {};

        this._providers = [];

        this._highlighter = new Highlighter();

        this._searchSettings = new Gio.Settings({schema_id: SEARCH_PROVIDERS_SCHEMA});
        this._searchSettings.connect('changed::disabled', this._reloadRemoteProviders.bind(this));
        this._searchSettings.connect('changed::enabled', this._reloadRemoteProviders.bind(this));
        this._searchSettings.connect('changed::disable-external', this._reloadRemoteProviders.bind(this));
        this._searchSettings.connect('changed::sort-order', this._reloadRemoteProviders.bind(this));

        this._searchTimeoutId = 0;
        this._cancellable = new Gio.Cancellable();
        this._searchCancelCancellable = new Gio.Cancellable();
        const cancellableCancelledId = this._cancellable.connect(() =>
            this._cancelSearchProviderRequest());
        this.connect('destroy', () => this._cancellable.disconnect(cancellableCancelledId));

        this._registerProvider(new AppDisplay.AppSearchProvider());

        let appSystem = Shell.AppSystem.get_default();
        appSystem.connect('installed-changed', this._reloadRemoteProviders.bind(this));
        this._reloadRemoteProviders();
    }

    get terms() {
        return this._terms;
    }

    _reloadRemoteProviders() {
        let remoteProviders = this._providers.filter(p => p.isRemoteProvider);
        remoteProviders.forEach(provider => {
            this._unregisterProvider(provider);
        });

        const providers = RemoteSearch.loadRemoteSearchProviders(this._searchSettings);
        providers.forEach(this._registerProvider.bind(this));
    }

    _registerProvider(provider) {
        provider.searchInProgress = false;

        // Filter out unwanted providers.
        if (provider.appInfo && !this._parentalControlsManager.shouldShowApp(provider.appInfo))
            return;

        this._providers.push(provider);
        this._ensureProviderDisplay(provider);
    }

    _unregisterProvider(provider) {
        let index = this._providers.indexOf(provider);
        this._providers.splice(index, 1);

        if (provider.display)
            provider.display.destroy();
    }

    _clearSearchTimeout() {
        if (this._searchTimeoutId > 0) {
            GLib.source_remove(this._searchTimeoutId);
            this._searchTimeoutId = 0;
        }
    }

    _cancelSearchProviderRequest() {
        if (this._terms.length !== 0 || this._searchCancelTimeoutId)
            return;

        this._searchCancelTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => {
            Promise.all(this._providers.map(async provider => {
                if (provider.isRemoteProvider &&
                    (provider.searchInProgress || provider.resultsMetasInProgress)) {
                    await provider.XUbuntuCancel(this._searchCancelCancellable);
                    provider.searchInProgress = false;
                    provider.resultsMetasInProgress = false;
                }
            })).catch(logError);

            delete this._searchCancelTimeoutId;
            return GLib.SOURCE_REMOVE;
        });
    }

    _reset() {
        this._terms = [];
        this._results = {};
        this._clearDisplay();
        this._clearSearchTimeout();
        this._cancelSearchProviderRequest();
        this._defaultResult = null;
        this._startingSearch = false;

        this._updateSearchProgress();
    }

    async _doProviderSearch(provider, previousResults) {
        provider.searchInProgress = true;

        let results;
        if (this._isSubSearch && previousResults) {
            results = await provider.getSubsearchResultSet(
                previousResults,
                this._terms,
                this._cancellable);
        } else {
            results = await provider.getInitialResultSet(
                this._terms,
                this._cancellable);
        }

        this._results[provider.id] = results;
        this._updateResults(provider, results);
    }

    _doSearch() {
        this._startingSearch = false;

        let previousResults = this._results;
        this._results = {};

        this._providers.forEach(provider => {
            let previousProviderResults = previousResults[provider.id];
            this._doProviderSearch(provider, previousProviderResults);
        });

        this._updateSearchProgress();

        this._clearSearchTimeout();
    }

    _onSearchTimeout() {
        this._searchTimeoutId = 0;
        this._doSearch();
        return GLib.SOURCE_REMOVE;
    }

    setTerms(terms) {
        // Check for the case of making a duplicate previous search before
        // setting state of the current search or cancelling the search.
        // This will prevent incorrect state being as a result of a duplicate
        // search while the previous search is still active.
        let searchString = terms.join(' ');
        let previousSearchString = this._terms.join(' ');
        if (searchString === previousSearchString)
            return;

        this._startingSearch = true;

        this._cancellable.cancel();
        this._cancellable.reset();

        if (terms.length === 0) {
            this._reset();
            return;
        }

        let isSubSearch = false;
        if (this._terms.length > 0)
            isSubSearch = searchString.indexOf(previousSearchString) === 0;

        this._searchCancelCancellable.cancel();
        this._searchCancelCancellable.reset();
        if (this._searchCancelTimeoutId) {
            GLib.source_remove(this._searchCancelTimeoutId);
            delete this._searchCancelTimeoutId;
        }

        this._terms = terms;
        this._isSubSearch = isSubSearch;
        this._updateSearchProgress();

        if (this._searchTimeoutId === 0)
            this._searchTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 150, this._onSearchTimeout.bind(this));

        this._highlighter = new Highlighter(this._terms);

        this.emit('terms-changed');
    }

    _onPan(action) {
        let [dist_, dx_, dy] = action.get_motion_delta(0);
        let adjustment = this._scrollView.vadjustment;
        adjustment.value -= (dy / this.height) * adjustment.page_size;
        return false;
    }

    _focusChildChanged(provider) {
        ensureActorVisibleInScrollView(this._scrollView, provider.focusChild);
    }

    _ensureProviderDisplay(provider) {
        if (provider.display)
            return;

        let providerDisplay;
        if (provider.appInfo)
            providerDisplay = new ListSearchResults(provider, this);
        else
            providerDisplay = new GridSearchResults(provider, this);

        providerDisplay.connect('notify::focus-child', this._focusChildChanged.bind(this));
        providerDisplay.hide();
        this._content.add_child(providerDisplay);
        provider.display = providerDisplay;
    }

    _clearDisplay() {
        this._providers.forEach(provider => {
            provider.display.clear();
        });
    }

    _maybeSetInitialSelection() {
        let newDefaultResult = null;

        let providers = this._providers;
        for (let i = 0; i < providers.length; i++) {
            let provider = providers[i];
            let display = provider.display;

            if (!display.visible)
                continue;

            let firstResult = display.getFirstResult();
            if (firstResult) {
                newDefaultResult = firstResult;
                break; // select this one!
            }
        }

        if (newDefaultResult !== this._defaultResult) {
            this._setSelected(this._defaultResult, false);
            this._setSelected(newDefaultResult, this._highlightDefault);

            this._defaultResult = newDefaultResult;
        }
    }

    get searchInProgress() {
        if (this._startingSearch)
            return true;

        return this._providers.some(p => p.searchInProgress);
    }

    _updateSearchProgress() {
        let haveResults = this._providers.some(provider => {
            let display = provider.display;
            return display.getFirstResult() != null;
        });

        this._scrollView.visible = haveResults;
        this._statusBin.visible = !haveResults;

        if (!haveResults) {
            if (this.searchInProgress)
                this._statusText.set_text(_('Searching…'));
            else
                this._statusText.set_text(_('No results.'));
        }
    }

    _updateResults(provider, results) {
        let terms = this._terms;
        let display = provider.display;

        display.updateSearch(results, terms, () => {
            provider.searchInProgress = false;

            this._maybeSetInitialSelection();
            this._updateSearchProgress();
        });
    }

    activateDefault() {
        // If we have a search queued up, force the search now.
        if (this._searchTimeoutId > 0)
            this._doSearch();

        if (this._defaultResult)
            this._defaultResult.activate();
    }

    highlightDefault(highlight) {
        this._highlightDefault = highlight;
        this._setSelected(this._defaultResult, highlight);
    }

    popupMenuDefault() {
        // If we have a search queued up, force the search now.
        if (this._searchTimeoutId > 0)
            this._doSearch();

        if (this._defaultResult)
            this._defaultResult.popup_menu();
    }

    navigateFocus(direction) {
        let rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
        if (direction === St.DirectionType.TAB_BACKWARD ||
            direction === (rtl
                ? St.DirectionType.RIGHT
                : St.DirectionType.LEFT) ||
            direction === St.DirectionType.UP) {
            this.navigate_focus(null, direction, false);
            return;
        }

        const from = this._defaultResult ?? null;
        this.navigate_focus(from, direction, false);
    }

    _setSelected(result, selected) {
        if (!result)
            return;

        if (selected) {
            result.add_style_pseudo_class('selected');
            ensureActorVisibleInScrollView(this._scrollView, result);
        } else {
            result.remove_style_pseudo_class('selected');
        }
    }

    highlightTerms(description) {
        if (!description)
            return '';

        return this._highlighter.highlight(description);
    }
});

const ProviderInfo = GObject.registerClass(
class ProviderInfo extends St.Button {
    _init(provider) {
        this.provider = provider;
        super._init({
            style_class: 'search-provider-icon',
            reactive: true,
            can_focus: true,
            accessible_name: provider.appInfo.get_name(),
            track_hover: true,
            y_align: Clutter.ActorAlign.START,
        });

        this._content = new St.BoxLayout({
            vertical: false,
            style_class: 'list-search-provider-content',
        });
        this.set_child(this._content);

        const icon = new St.Icon({
            icon_size: this.PROVIDER_ICON_SIZE,
            gicon: provider.appInfo.get_icon(),
        });

        const detailsBox = new St.BoxLayout({
            style_class: 'list-search-provider-details',
            vertical: true,
            x_expand: true,
        });

        const nameLabel = new St.Label({
            text: provider.appInfo.get_name(),
            x_align: Clutter.ActorAlign.START,
        });

        this._moreLabel = new St.Label({x_align: Clutter.ActorAlign.START});

        detailsBox.add_child(nameLabel);
        detailsBox.add_child(this._moreLabel);


        this._content.add_child(icon);
        this._content.add_child(detailsBox);
    }

    get PROVIDER_ICON_SIZE() {
        return 32;
    }

    animateLaunch() {
        let appSys = Shell.AppSystem.get_default();
        let app = appSys.lookup_app(this.provider.appInfo.get_id());
        if (app.state === Shell.AppState.STOPPED)
            IconGrid.zoomOutActor(this._content);
    }

    setMoreCount(count) {
        this._moreLabel.text = ngettext('%d more', '%d more', count).format(count);
        this._moreLabel.visible = count > 0;
    }
});
(uuay)keyring.js // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import Gcr from 'gi://Gcr';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as Dialog from '../dialog.js';
import * as ModalDialog from '../modalDialog.js';
import * as ShellEntry from '../shellEntry.js';
import * as CheckBox from '../checkBox.js';
import {wiggle} from '../../misc/animationUtils.js';

const KeyringDialog = GObject.registerClass(
class KeyringDialog extends ModalDialog.ModalDialog {
    _init() {
        super._init({styleClass: 'prompt-dialog'});

        this.prompt = new Shell.KeyringPrompt();
        this.prompt.connect('show-password', this._onShowPassword.bind(this));
        this.prompt.connect('show-confirm', this._onShowConfirm.bind(this));
        this.prompt.connect('prompt-close', this._onHidePrompt.bind(this));

        let content = new Dialog.MessageDialogContent();

        this.prompt.bind_property('message',
            content, 'title', GObject.BindingFlags.SYNC_CREATE);
        this.prompt.bind_property('description',
            content, 'description', GObject.BindingFlags.SYNC_CREATE);

        let passwordBox = new St.BoxLayout({
            style_class: 'prompt-dialog-password-layout',
            vertical: true,
        });

        this._passwordEntry = new St.PasswordEntry({
            style_class: 'prompt-dialog-password-entry',
            can_focus: true,
            x_align: Clutter.ActorAlign.CENTER,
        });
        ShellEntry.addContextMenu(this._passwordEntry);
        this._passwordEntry.clutter_text.connect('activate', this._onPasswordActivate.bind(this));
        this.prompt.bind_property('password-visible',
            this._passwordEntry, 'visible', GObject.BindingFlags.SYNC_CREATE);
        passwordBox.add_child(this._passwordEntry);

        this._confirmEntry = new St.PasswordEntry({
            style_class: 'prompt-dialog-password-entry',
            can_focus: true,
            x_align: Clutter.ActorAlign.CENTER,
        });
        ShellEntry.addContextMenu(this._confirmEntry);
        this._confirmEntry.clutter_text.connect('activate', this._onConfirmActivate.bind(this));
        this.prompt.bind_property('confirm-visible',
            this._confirmEntry, 'visible', GObject.BindingFlags.SYNC_CREATE);
        passwordBox.add_child(this._confirmEntry);

        this.prompt.set_password_actor(this._passwordEntry.clutter_text);
        this.prompt.set_confirm_actor(this._confirmEntry.clutter_text);

        let warningBox = new St.BoxLayout({vertical: true});

        let capsLockWarning = new ShellEntry.CapsLockWarning();
        let syncCapsLockWarningVisibility = () => {
            capsLockWarning.visible =
                this.prompt.password_visible || this.prompt.confirm_visible;
        };
        this.prompt.connect('notify::password-visible', syncCapsLockWarningVisibility);
        this.prompt.connect('notify::confirm-visible', syncCapsLockWarningVisibility);
        warningBox.add_child(capsLockWarning);

        let warning = new St.Label({style_class: 'prompt-dialog-error-label'});
        warning.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        warning.clutter_text.line_wrap = true;
        this.prompt.bind_property('warning',
            warning, 'text', GObject.BindingFlags.SYNC_CREATE);
        this.prompt.connect('notify::warning-visible', () => {
            warning.opacity = this.prompt.warning_visible ? 255 : 0;
        });
        this.prompt.connect('notify::warning', () => {
            if (this._passwordEntry && this.prompt.warning !== '')
                wiggle(this._passwordEntry);
        });
        warningBox.add_child(warning);

        passwordBox.add_child(warningBox);
        content.add_child(passwordBox);

        this._choice = new CheckBox.CheckBox();
        this.prompt.bind_property('choice-label', this._choice.getLabelActor(),
            'text', GObject.BindingFlags.SYNC_CREATE);
        this.prompt.bind_property('choice-chosen', this._choice,
            'checked', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);
        this.prompt.bind_property('choice-visible', this._choice,
            'visible', GObject.BindingFlags.SYNC_CREATE);
        content.add_child(this._choice);

        this.contentLayout.add_child(content);

        this._cancelButton = this.addButton({
            label: '',
            action: this._onCancelButton.bind(this),
            key: Clutter.KEY_Escape,
        });
        this._continueButton = this.addButton({
            label: '',
            action: this._onContinueButton.bind(this),
            default: true,
        });

        this.prompt.bind_property('cancel-label', this._cancelButton, 'label', GObject.BindingFlags.SYNC_CREATE);
        this.prompt.bind_property('continue-label', this._continueButton, 'label', GObject.BindingFlags.SYNC_CREATE);
    }

    _updateSensitivity(sensitive) {
        if (this._passwordEntry)
            this._passwordEntry.reactive = sensitive;

        if (this._confirmEntry)
            this._confirmEntry.reactive = sensitive;

        this._continueButton.can_focus = sensitive;
        this._continueButton.reactive = sensitive;
    }

    _ensureOpen() {
        // NOTE: ModalDialog.open() is safe to call if the dialog is
        // already open - it just returns true without side-effects
        if (this.open())
            return true;

        // The above fail if e.g. unable to get input grab
        //
        // In an ideal world this wouldn't happen (because the
        // Shell is in complete control of the session) but that's
        // just not how things work right now.

        log('keyringPrompt: Failed to show modal dialog.' +
            ' Dismissing prompt request');
        this.prompt.cancel();
        return false;
    }

    _onShowPassword() {
        this._ensureOpen();
        this._updateSensitivity(true);
        this._passwordEntry.text = '';
        this._passwordEntry.grab_key_focus();
    }

    _onShowConfirm() {
        this._ensureOpen();
        this._updateSensitivity(true);
        this._confirmEntry.text = '';
        this._continueButton.grab_key_focus();
    }

    _onHidePrompt() {
        this.close();
    }

    _onPasswordActivate() {
        if (this.prompt.confirm_visible)
            this._confirmEntry.grab_key_focus();
        else
            this._onContinueButton();
    }

    _onConfirmActivate() {
        this._onContinueButton();
    }

    _onContinueButton() {
        this._updateSensitivity(false);
        this.prompt.complete();
    }

    _onCancelButton() {
        this.prompt.cancel();
    }
});

class KeyringDummyDialog {
    constructor() {
        this.prompt = new Shell.KeyringPrompt();
        this.prompt.connect('show-password', this._cancelPrompt.bind(this));
        this.prompt.connect('show-confirm', this._cancelPrompt.bind(this));
    }

    _cancelPrompt() {
        this.prompt.cancel();
    }
}

const KeyringPrompter = GObject.registerClass(
class KeyringPrompter extends Gcr.SystemPrompter {
    _init() {
        super._init();
        this.connect('new-prompt', () => {
            let dialog = this._enabled
                ? new KeyringDialog()
                : new KeyringDummyDialog();
            this._currentPrompt = dialog.prompt;
            return this._currentPrompt;
        });
        this._dbusId = null;
        this._registered = false;
        this._enabled = false;
        this._currentPrompt = null;
    }

    enable() {
        if (!this._registered) {
            this.register(Gio.DBus.session);
            this._dbusId = Gio.DBus.session.own_name('org.gnome.keyring.SystemPrompter',
                Gio.BusNameOwnerFlags.ALLOW_REPLACEMENT, null, null);
            this._registered = true;
        }
        this._enabled = true;
    }

    disable() {
        this._enabled = false;

        if (this.prompting)
            this._currentPrompt.cancel();
        this._currentPrompt = null;
    }
});

export {KeyringPrompter as Component};
(uuay)shell/�permissionStore.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';

import {loadInterfaceXML} from './fileUtils.js';

const PermissionStoreIface = loadInterfaceXML('org.freedesktop.impl.portal.PermissionStore');
const PermissionStoreProxy = Gio.DBusProxy.makeProxyWrapper(PermissionStoreIface);

/**
 * @param {Function} initCallback
 * @param {Gio.Cancellable} cancellable
 * @returns {Gio.DBusProxy}
 */
export function PermissionStore(initCallback, cancellable) {
    return new PermissionStoreProxy(Gio.DBus.session,
        'org.freedesktop.impl.portal.PermissionStore',
        '/org/freedesktop/impl/portal/PermissionStore',
        initCallback, cancellable);
}
(uuay)scripting.js1// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';

import * as Config from '../misc/config.js';
import * as Main from './main.js';
import * as Params from '../misc/params.js';
import * as Util from '../misc/util.js';

import {loadInterfaceXML} from '../misc/fileUtils.js';

// This module provides functionality for driving the shell user interface
// in an automated fashion. The primary current use case for this is
// automated performance testing (see runPerfScript()), but it could
// be applied to other forms of automation, such as testing for
// correctness as well.
//
// When scripting an automated test we want to make a series of calls
// in a linear fashion, but we also want to be able to let the main
// loop run so actions can finish. For this reason we write the script
// as an async function that uses await when it wants to let the main
// loop run.
//
//    await Scripting.sleep(1000);
//    main.overview.show();
//    await Scripting.waitLeisure();
//

/**
 * Used within an automation script to pause the the execution of the
 * current script for the specified amount of time. Use as
 * 'yield Scripting.sleep(500);'
 *
 * @param {number} milliseconds - number of milliseconds to wait
 * @returns {Promise} that resolves after @milliseconds ms
 */
export function sleep(milliseconds) {
    return new Promise(resolve => {
        let id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, milliseconds, () => {
            resolve();
            return GLib.SOURCE_REMOVE;
        });
        GLib.Source.set_name_by_id(id, '[gnome-shell] sleep');
    });
}

/**
 * Used within an automation script to pause the the execution of the
 * current script until the shell is completely idle. Use as
 * 'yield Scripting.waitLeisure();'
 *
 * @returns {Promise} that resolves when the shell is idle
 */
export function waitLeisure() {
    return new Promise(resolve => {
        global.run_at_leisure(resolve);
    });
}

const PerfHelperIface = loadInterfaceXML('org.gnome.Shell.PerfHelper');
export const PerfHelperProxy = Gio.DBusProxy.makeProxyWrapper(PerfHelperIface);

let _perfHelper = null;

/**
 * @returns {PerfHelper}
 */
export async function _getPerfHelper() {
    if (_perfHelper == null) {
        _perfHelper = await PerfHelperProxy.newAsync(
            Gio.DBus.session, 'org.gnome.Shell.PerfHelper', '/org/gnome/Shell/PerfHelper');
        _perfHelper._autoExit = true;
    }

    return _perfHelper;
}

/** @private */
export function _spawnPerfHelper() {
    let path = GLib.getenv('GNOME_SHELL_BUILDDIR') || Config.LIBEXECDIR;
    let command = `${path}/gnome-shell-perf-helper`;
    Util.trySpawnCommandLine(command);
}

/**
 * createTestWindow:
 *
 * @param {object} params options for window creation.
 * @param {number} [params.width=640] - width of window, in pixels
 * @param {number} [params.height=480] - height of window, in pixels
 * @param {boolean} [params.alpha=false] - whether the window should have an alpha channel
 * @param {boolean} [params.maximized=false] - whether the window should be created maximized
 * @param {boolean} [params.redraws=false] - whether the window should continually redraw itself
 * @returns {Promise}
 *
 * Creates a window using gnome-shell-perf-helper for testing purposes.
 * While this function can be used with yield in an automation
 * script to pause until the D-Bus call to the helper process returns,
 * because of the normal X asynchronous mapping process, to actually wait
 * until the window has been mapped and exposed, use waitTestWindows().
 */
export async function createTestWindow(params) {
    params = Params.parse(params, {
        width: 640,
        height: 480,
        alpha: false,
        maximized: false,
        redraws: false,
        textInput: false,
    });

    let perfHelper = await _getPerfHelper();
    perfHelper.CreateWindowAsync(
        params.width, params.height,
        params.alpha, params.maximized,
        params.redraws, params.textInput).catch(logError);
}

/**
 * waitTestWindows:
 *
 * @returns {Promise}
 *
 * Used within an automation script to pause until all windows previously
 * created with createTestWindow have been mapped and exposed.
 */
export async function waitTestWindows() {
    let perfHelper = await _getPerfHelper();
    return perfHelper.WaitWindowsAsync().catch(logError);
}

/**
 * destroyTestWindows:
 *
 * @returns {Promise}
 *
 * Destroys all windows previously created with createTestWindow().
 * While this function can be used with yield in an automation
 * script to pause until the D-Bus call to the helper process returns,
 * this doesn't guarantee that Mutter has actually finished the destroy
 * process because of normal X asynchronicity.
 */
export async function destroyTestWindows() {
    let perfHelper = await _getPerfHelper();
    return perfHelper.DestroyWindowsAsync().catch(logError);
}

/**
 * disableHelperAutoExit:
 *
 * Don't exixt the perf helper after running the script. Instead it will remain
 * running until something else makes it exit, e.g. the Wayland socket closing.
 */
export async function disableHelperAutoExit() {
    let perfHelper = await _getPerfHelper();
    perfHelper._autoExit = false;
}

/**
 * defineScriptEvent:
 *
 * @param {string} name The event will be called script.<name>
 * @param {string} description Short human-readable description of the event
 *
 * Convenience function to define a zero-argument performance event
 * within the 'script' namespace that is reserved for events defined locally
 * within a performance automation script
 */
export function defineScriptEvent(name, description) {
    Shell.PerfLog.get_default().define_event(
        `script.${name}`, description, '');
}

/**
 * scriptEvent
 *
 * @param {string} name Name registered with defineScriptEvent()
 *
 * Convenience function to record a script-local performance event
 * previously defined with defineScriptEvent
 */
export function scriptEvent(name) {
    Shell.PerfLog.get_default().event(`script.${name}`);
}

/**
 * collectStatistics
 *
 * Convenience function to trigger statistics collection
 */
export function collectStatistics() {
    Shell.PerfLog.get_default().collect_statistics();
}

function _collect(scriptModule, outputFile) {
    let eventHandlers = {};

    for (let f in scriptModule) {
        let m = /([A-Za-z]+)_([A-Za-z]+)/.exec(f);
        if (m)
            eventHandlers[`${m[1]}.${m[2]}`] = scriptModule[f];
    }

    Shell.PerfLog.get_default().replay(
        (time, eventName, signature, arg) => {
            if (eventName in eventHandlers)
                eventHandlers[eventName](time, arg);
        });

    if ('finish' in scriptModule)
        scriptModule.finish();

    if (outputFile) {
        let f = Gio.file_new_for_path(outputFile);
        let raw = f.replace(null,
            false,
            Gio.FileCreateFlags.NONE,
            null);
        let out = Gio.BufferedOutputStream.new_sized(raw, 4096);
        Shell.write_string_to_stream(out, '{\n');

        Shell.write_string_to_stream(out, '"events":\n');
        Shell.PerfLog.get_default().dump_events(out);

        let monitors = Main.layoutManager.monitors;
        let primary = Main.layoutManager.primaryIndex;
        Shell.write_string_to_stream(out, ',\n"monitors":\n[');
        for (let i = 0; i < monitors.length; i++) {
            let monitor = monitors[i];
            if (i !== 0)
                Shell.write_string_to_stream(out, ', ');
            const prefix = i === primary ? '*' : '';
            Shell.write_string_to_stream(out,
                `"${prefix}${monitor.width}x${monitor.height}+${monitor.x}+${monitor.y}"`);
        }
        Shell.write_string_to_stream(out, ' ]');

        Shell.write_string_to_stream(out, ',\n"metrics":\n[ ');
        let first = true;
        for (let name in scriptModule.METRICS) {
            let metric = scriptModule.METRICS[name];
            // Extra checks here because JSON.stringify generates
            // invalid JSON for undefined values
            if (metric.description == null) {
                log(`Error: No description found for metric ${name}`);
                continue;
            }
            if (metric.units == null) {
                log(`Error: No units found for metric ${name}`);
                continue;
            }
            if (metric.value == null) {
                log(`Error: No value found for metric ${name}`);
                continue;
            }

            if (!first)
                Shell.write_string_to_stream(out, ',\n  ');
            first = false;

            Shell.write_string_to_stream(out,
                `{ "name": ${JSON.stringify(name)},\n` +
                `    "description": ${JSON.stringify(metric.description)},\n` +
                `    "units": ${JSON.stringify(metric.units)},\n` +
                `    "value": ${JSON.stringify(metric.value)} }`);
        }
        Shell.write_string_to_stream(out, ' ]');

        Shell.write_string_to_stream(out, ',\n"log":\n');
        Shell.PerfLog.get_default().dump_log(out);

        Shell.write_string_to_stream(out, '\n}\n');
        out.close(null);
    } else {
        let metrics = [];
        for (let metric in scriptModule.METRICS)
            metrics.push(metric);

        metrics.sort();

        print('------------------------------------------------------------');
        for (let i = 0; i < metrics.length; i++) {
            let metric = metrics[i];
            print(`# ${scriptModule.METRICS[metric].description}`);
            print(`${metric}: ${scriptModule.METRICS[metric].value}${scriptModule.METRICS[metric].units}`);
        }
        print('------------------------------------------------------------');
    }
}

async function _runPerfScript(scriptModule, outputFile) {
    try {
        await scriptModule.run();
    } catch (err) {
        logError(err, 'Script failed');
        Meta.exit(Meta.ExitCode.ERROR);
    }

    try {
        _collect(scriptModule, outputFile);
    } catch (err) {
        logError(err, 'Script failed');
        Meta.exit(Meta.ExitCode.ERROR);
    }

    try {
        const perfHelper = await _getPerfHelper();
        if (perfHelper._autoExit)
            perfHelper.ExitSync();
    } catch (err) {
        logError(err, 'Failed to exit helper');
        Meta.exit(Meta.ExitCode.ERROR);
    }

    global.context.terminate();
}

/**
 * runPerfScript
 *
 * Runs a script for automated collection of performance data. The
 * script is defined as a Javascript module with specified contents.
 *
 * First the run() function within the module will be called as a
 * generator to automate a series of actions. These actions will
 * trigger performance events and the script can also record its
 * own performance events.
 *
 * Then the recorded event log is replayed using handler functions
 * within the module. The handler for the event 'foo.bar' is called
 * foo_bar().
 *
 * Finally if the module has a function called finish(), that will
 * be called.
 *
 * The event handler and finish functions are expected to fill in
 * metrics to an object within the module called METRICS. Each
 * property of this object represents an individual metric. The
 * name of the property is the name of the metric, the value
 * of the property is an object with the following properties:
 *
 *  description: human readable description of the metric
 *  units: a string representing the units of the metric. It has
 *   the form '<unit> <unit> ... / <unit> / <unit> ...'. Certain
 *   unit values are recognized: s, ms, us, B, KiB, MiB. Other
 *   values can appear but are uninterpreted. Examples 's',
 *   '/ s', 'frames', 'frames / s', 'MiB / s / frame'
 *  value: computed value of the metric
 *
 * The resulting metrics will be written to `outputFile` as JSON, or,
 * if `outputFile` is not provided, logged.
 *
 * After running the script and collecting statistics from the
 * event log, GNOME Shell will exit.
 *
 * @param {object} scriptModule module object with run and finish
 *   functions and event handlers
 * @param {string} outputFile path to write output to
 */
export function runPerfScript(scriptModule, outputFile) {
    Shell.PerfLog.get_default().set_enabled(true);
    _spawnPerfHelper();

    Gio.bus_watch_name(Gio.BusType.SESSION,
        'org.gnome.Shell.PerfHelper',
        Gio.BusNameWatcherFlags.NONE,
        () => _runPerfScript(scriptModule, outputFile),
        null);
}
(uuay)shellEntry.js�// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

import * as BoxPointer from './boxpointer.js';
import * as Main from './main.js';
import * as Params from '../misc/params.js';
import * as PopupMenu from './popupMenu.js';

export class EntryMenu extends PopupMenu.PopupMenu {
    constructor(entry) {
        super(entry, 0, St.Side.TOP);

        this._entry = entry;
        this._clipboard = St.Clipboard.get_default();

        // Populate menu
        let item;
        item = new PopupMenu.PopupMenuItem(_('Copy'));
        item.connect('activate', this._onCopyActivated.bind(this));
        this.addMenuItem(item);
        this._copyItem = item;

        item = new PopupMenu.PopupMenuItem(_('Paste'));
        item.connect('activate', this._onPasteActivated.bind(this));
        this.addMenuItem(item);
        this._pasteItem = item;

        if (entry instanceof St.PasswordEntry)
            this._makePasswordItem();

        Main.uiGroup.add_child(this.actor);
        this.actor.hide();
    }

    _makePasswordItem() {
        let item = new PopupMenu.PopupMenuItem('');
        item.connect('activate', this._onPasswordActivated.bind(this));
        this.addMenuItem(item);
        this._passwordItem = item;

        this._entry.bind_property('show-peek-icon',
            this._passwordItem, 'visible',
            GObject.BindingFlags.SYNC_CREATE);
    }

    open(animate) {
        this._updatePasteItem();
        this._updateCopyItem();
        if (this._passwordItem)
            this._updatePasswordItem();

        super.open(animate);
        this._entry.add_style_pseudo_class('focus');

        let direction = St.DirectionType.TAB_FORWARD;
        if (!this.actor.navigate_focus(null, direction, false))
            this.actor.grab_key_focus();
    }

    _updateCopyItem() {
        let selection = this._entry.clutter_text.get_selection();
        this._copyItem.setSensitive(!this._entry.clutter_text.password_char &&
                                    selection && selection !== '');
    }

    _updatePasteItem() {
        this._clipboard.get_text(St.ClipboardType.CLIPBOARD,
            (clipboard, text) => {
                this._pasteItem.setSensitive(text && text !== '');
            });
    }

    _updatePasswordItem() {
        if (!this._entry.password_visible)
            this._passwordItem.label.set_text(_('Show Text'));
        else
            this._passwordItem.label.set_text(_('Hide Text'));
    }

    _onCopyActivated() {
        let selection = this._entry.clutter_text.get_selection();
        this._clipboard.set_text(St.ClipboardType.CLIPBOARD, selection);
    }

    _onPasteActivated() {
        this._clipboard.get_text(St.ClipboardType.CLIPBOARD,
            (clipboard, text) => {
                if (!text)
                    return;
                this._entry.clutter_text.delete_selection();
                let pos = this._entry.clutter_text.get_cursor_position();
                this._entry.clutter_text.insert_text(text, pos);
            });
    }

    _onPasswordActivated() {
        this._entry.password_visible  = !this._entry.password_visible;
    }
}

function _setMenuAlignment(entry, stageX) {
    let [success, entryX] = entry.transform_stage_point(stageX, 0);
    if (success)
        entry.menu.setSourceAlignment(entryX / entry.width);
}

function _onButtonPressEvent(actor, event, entry) {
    if (entry.menu.isOpen) {
        entry.menu.close(BoxPointer.PopupAnimation.FULL);
        return Clutter.EVENT_STOP;
    } else if (event.get_button() === 3) {
        let [stageX] = event.get_coords();
        _setMenuAlignment(entry, stageX);
        entry.menu.open(BoxPointer.PopupAnimation.FULL);
        return Clutter.EVENT_STOP;
    }
    return Clutter.EVENT_PROPAGATE;
}

function _onPopup(actor, entry) {
    let cursorPosition = entry.clutter_text.get_cursor_position();
    let [success, textX, textY_, lineHeight_] = entry.clutter_text.position_to_coords(cursorPosition);
    if (success)
        entry.menu.setSourceAlignment(textX / entry.width);
    entry.menu.open(BoxPointer.PopupAnimation.FULL);
}

/**
 * @param {St.Entry} entry
 * @param {*} params
 */
export function addContextMenu(entry, params) {
    if (entry.menu)
        return;

    params = Params.parse(params, {actionMode: Shell.ActionMode.POPUP});

    entry.menu = new EntryMenu(entry);
    entry._menuManager = new PopupMenu.PopupMenuManager(entry, {
        actionMode: params.actionMode,
    });
    entry._menuManager.addMenu(entry.menu);

    // Add an event handler to both the entry and its clutter_text; the former
    // so padding is included in the clickable area, the latter because the
    // event processing of ClutterText prevents event-bubbling.
    entry.clutter_text.connect('button-press-event', (actor, event) => {
        _onButtonPressEvent(actor, event, entry);
    });
    entry.connect('button-press-event', (actor, event) => {
        _onButtonPressEvent(actor, event, entry);
    });

    entry.connect('popup-menu', actor => _onPopup(actor, entry));

    entry.connect('destroy', () => {
        entry.menu.destroy();
        entry.menu = null;
        entry._menuManager = null;
    });
}

export const CapsLockWarning = GObject.registerClass(
class CapsLockWarning extends St.Label {
    _init(params) {
        super._init({
            style_class: 'caps-lock-warning-label',
            ...params,
        });

        this.text = _('Caps lock is on.');

        this.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
        this.clutter_text.line_wrap = true;

        let seat = Clutter.get_default_backend().get_default_seat();
        this._keymap = seat.get_keymap();

        this.connect('notify::mapped', () => {
            if (this.is_mapped()) {
                this._keymap.connectObject(
                    'state-changed', () => this._sync(true), this);
            } else {
                this._keymap.disconnectObject(this);
            }

            this._sync(false);
        });
    }

    _sync(animate) {
        let capsLockOn = this._keymap.get_caps_lock_state();

        this.remove_all_transitions();

        const {naturalHeightSet} = this;
        this.natural_height_set = false;
        let [, height] = this.get_preferred_height(-1);
        this.natural_height_set = naturalHeightSet;

        this.ease({
            height: capsLockOn ? height : 0,
            opacity: capsLockOn ? 255 : 0,
            duration: animate ? 200 : 0,
            onComplete: () => {
                if (capsLockOn)
                    this.height = -1;
            },
        });
    }
});
(uuay)ibusCandidatePopup.js/0// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-

import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import IBus from 'gi://IBus';
import St from 'gi://St';

import * as BoxPointer from './boxpointer.js';
import * as Main from './main.js';

const MAX_CANDIDATES_PER_PAGE = 16;

const DEFAULT_INDEX_LABELS = [
    '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
    'a', 'b', 'c', 'd', 'e', 'f',
];

const CandidateArea = GObject.registerClass({
    Signals: {
        'candidate-clicked': {
            param_types: [
                GObject.TYPE_UINT, GObject.TYPE_UINT, Clutter.ModifierType.$gtype,
            ],
        },
        'cursor-down': {},
        'cursor-up': {},
        'next-page': {},
        'previous-page': {},
    },
}, class CandidateArea extends St.BoxLayout {
    _init() {
        super._init({
            vertical: true,
            reactive: true,
            visible: false,
        });
        this._candidateBoxes = [];
        for (let i = 0; i < MAX_CANDIDATES_PER_PAGE; ++i) {
            const box = new St.BoxLayout({
                style_class: 'candidate-box',
                reactive: true,
                track_hover: true,
            });
            box._indexLabel = new St.Label({style_class: 'candidate-index'});
            box._candidateLabel = new St.Label({style_class: 'candidate-label'});
            box.add_child(box._indexLabel);
            box.add_child(box._candidateLabel);
            this._candidateBoxes.push(box);
            this.add_child(box);

            let j = i;
            box.connect('button-release-event', (actor, event) => {
                this.emit('candidate-clicked', j, event.get_button(), event.get_state());
                return Clutter.EVENT_PROPAGATE;
            });
        }

        this._buttonBox = new St.BoxLayout({style_class: 'candidate-page-button-box'});

        this._previousButton = new St.Button({
            style_class: 'candidate-page-button candidate-page-button-previous button',
            x_expand: true,
        });
        this._buttonBox.add_child(this._previousButton);

        this._nextButton = new St.Button({
            style_class: 'candidate-page-button candidate-page-button-next button',
            x_expand: true,
        });
        this._buttonBox.add_child(this._nextButton);

        this.add_child(this._buttonBox);

        this._previousButton.connect('clicked', () => {
            this.emit('previous-page');
        });
        this._nextButton.connect('clicked', () => {
            this.emit('next-page');
        });

        this._orientation = -1;
        this._cursorPosition = 0;
    }

    vfunc_scroll_event(event) {
        switch (event.get_scroll_direction()) {
        case Clutter.ScrollDirection.UP:
            this.emit('cursor-up');
            break;
        case Clutter.ScrollDirection.DOWN:
            this.emit('cursor-down');
            break;
        }
        return Clutter.EVENT_PROPAGATE;
    }

    setOrientation(orientation) {
        if (this._orientation === orientation)
            return;

        this._orientation = orientation;

        if (this._orientation === IBus.Orientation.HORIZONTAL) {
            this.vertical = false;
            this.remove_style_class_name('vertical');
            this.add_style_class_name('horizontal');
            this._previousButton.icon_name = 'go-previous-symbolic';
            this._nextButton.icon_name = 'go-next-symbolic';
        } else {                // VERTICAL || SYSTEM
            this.vertical = true;
            this.add_style_class_name('vertical');
            this.remove_style_class_name('horizontal');
            this._previousButton.icon_name = 'go-up-symbolic';
            this._nextButton.icon_name = 'go-down-symbolic';
        }
    }

    setCandidates(indexes, candidates, cursorPosition, cursorVisible) {
        for (let i = 0; i < MAX_CANDIDATES_PER_PAGE; ++i) {
            let visible = i < candidates.length;
            let box = this._candidateBoxes[i];
            box.visible = visible;

            if (!visible)
                continue;

            box._indexLabel.text = indexes && indexes[i] ? indexes[i] : DEFAULT_INDEX_LABELS[i];
            box._candidateLabel.text = candidates[i];
        }

        this._candidateBoxes[this._cursorPosition].remove_style_pseudo_class('selected');
        this._cursorPosition = cursorPosition;
        if (cursorVisible)
            this._candidateBoxes[cursorPosition].add_style_pseudo_class('selected');
    }

    updateButtons(wrapsAround, page, nPages) {
        if (nPages < 2) {
            this._buttonBox.hide();
            return;
        }
        this._buttonBox.show();
        this._previousButton.reactive = wrapsAround || page > 0;
        this._nextButton.reactive = wrapsAround || page < nPages - 1;
    }
});

export const CandidatePopup = GObject.registerClass(
class IbusCandidatePopup extends BoxPointer.BoxPointer {
    _init() {
        super._init(St.Side.TOP);
        this.visible = false;
        this.style_class = 'candidate-popup-boxpointer';

        this._dummyCursor = new Clutter.Actor({opacity: 0});
        Main.layoutManager.uiGroup.add_child(this._dummyCursor);

        Main.layoutManager.addTopChrome(this);

        const box = new St.BoxLayout({
            style_class: 'candidate-popup-content',
            vertical: true,
        });
        this.bin.set_child(box);

        this._preeditText = new St.Label({
            style_class: 'candidate-popup-text',
            visible: false,
        });
        box.add_child(this._preeditText);

        this._auxText = new St.Label({
            style_class: 'candidate-popup-text',
            visible: false,
        });
        box.add_child(this._auxText);

        this._candidateArea = new CandidateArea();
        box.add_child(this._candidateArea);

        this._candidateArea.connect('previous-page', () => {
            this._panelService.page_up();
        });
        this._candidateArea.connect('next-page', () => {
            this._panelService.page_down();
        });

        this._candidateArea.connect('cursor-up', () => {
            this._panelService.cursor_up();
        });
        this._candidateArea.connect('cursor-down', () => {
            this._panelService.cursor_down();
        });

        this._candidateArea.connect('candidate-clicked', (area, index, button, state) => {
            this._panelService.candidate_clicked(index, button, state);
        });

        this._panelService = null;
    }

    setPanelService(panelService) {
        this._panelService = panelService;
        if (!panelService)
            return;

        panelService.connect('set-cursor-location', (ps, x, y, w, h) => {
            this._setDummyCursorGeometry(x, y, w, h);
        });
        try {
            panelService.connect('set-cursor-location-relative', (ps, x, y, w, h) => {
                if (!global.display.focus_window)
                    return;
                let window = global.display.focus_window.get_compositor_private();
                this._setDummyCursorGeometry(window.x + x, window.y + y, w, h);
            });
        } catch (e) {
            // Only recent IBus versions have support for this signal
            // which is used for wayland clients. In order to work
            // with older IBus versions we can silently ignore the
            // signal's absence.
        }
        panelService.connect('update-preedit-text', (ps, text, cursorPosition, visible) => {
            this._preeditText.visible = visible;
            this._updateVisibility();

            this._preeditText.text = text.get_text();

            let attrs = text.get_attributes();
            if (attrs)
                this._setTextAttributes(this._preeditText.clutter_text, attrs);
        });
        panelService.connect('show-preedit-text', () => {
            this._preeditText.show();
            this._updateVisibility();
        });
        panelService.connect('hide-preedit-text', () => {
            this._preeditText.hide();
            this._updateVisibility();
        });
        panelService.connect('update-auxiliary-text', (_ps, text, visible) => {
            this._auxText.visible = visible;
            this._updateVisibility();

            this._auxText.text = text.get_text();
        });
        panelService.connect('show-auxiliary-text', () => {
            this._auxText.show();
            this._updateVisibility();
        });
        panelService.connect('hide-auxiliary-text', () => {
            this._auxText.hide();
            this._updateVisibility();
        });
        panelService.connect('update-lookup-table', (_ps, lookupTable, visible) => {
            this._candidateArea.visible = visible;
            this._updateVisibility();

            let nCandidates = lookupTable.get_number_of_candidates();
            let cursorPos = lookupTable.get_cursor_pos();
            let pageSize = lookupTable.get_page_size();
            let nPages = Math.ceil(nCandidates / pageSize);
            let page = cursorPos === 0 ? 0 : Math.floor(cursorPos / pageSize);
            let startIndex = page * pageSize;
            let endIndex = Math.min((page + 1) * pageSize, nCandidates);

            let indexes = [];
            let indexLabel;
            for (let i = 0; (indexLabel = lookupTable.get_label(i)); ++i)
                indexes.push(indexLabel.get_text());

            Main.keyboard.resetSuggestions();
            Main.keyboard.setSuggestionsVisible(visible);

            let candidates = [];
            for (let i = startIndex; i < endIndex; ++i) {
                candidates.push(lookupTable.get_candidate(i).get_text());

                Main.keyboard.addSuggestion(lookupTable.get_candidate(i).get_text(), () => {
                    let index = i;
                    this._panelService.candidate_clicked(index, 1, 0);
                });
            }

            this._candidateArea.setCandidates(indexes,
                candidates,
                cursorPos % pageSize,
                lookupTable.is_cursor_visible());
            this._candidateArea.setOrientation(lookupTable.get_orientation());
            this._candidateArea.updateButtons(lookupTable.is_round(), page, nPages);
        });
        panelService.connect('show-lookup-table', () => {
            Main.keyboard.setSuggestionsVisible(true);
            this._candidateArea.show();
            this._updateVisibility();
        });
        panelService.connect('hide-lookup-table', () => {
            Main.keyboard.setSuggestionsVisible(false);
            this._candidateArea.hide();
            this._updateVisibility();
        });
        panelService.connect('focus-out', () => {
            this.close(BoxPointer.PopupAnimation.NONE);
            Main.keyboard.resetSuggestions();
        });
    }

    _setDummyCursorGeometry(x, y, w, h) {
        this._dummyCursor.set_position(Math.round(x), Math.round(y));
        this._dummyCursor.set_size(Math.round(w), Math.round(h));

        if (this.visible)
            this.setPosition(this._dummyCursor, 0);
    }

    _updateVisibility() {
        let isVisible = !Main.keyboard.visible &&
                         (this._preeditText.visible ||
                          this._auxText.visible ||
                          this._candidateArea.visible);

        if (isVisible) {
            this.setPosition(this._dummyCursor, 0);
            this.open(BoxPointer.PopupAnimation.NONE);
            // We shouldn't be above some components like the screenshot UI,
            // so don't raise to the top.
            // The on-screen keyboard is expected to be above any entries,
            // so just above the keyboard gets us to the right layer.
            const {keyboardBox} = Main.layoutManager;
            this.get_parent().set_child_above_sibling(this, keyboardBox);
        } else {
            this.close(BoxPointer.PopupAnimation.NONE);
        }
    }

    _setTextAttributes(clutterText, ibusAttrList) {
        let attr;
        for (let i = 0; (attr = ibusAttrList.get(i)); ++i) {
            if (attr.get_attr_type() === IBus.AttrType.BACKGROUND)
                clutterText.set_selection(attr.get_start_index(), attr.get_end_index());
        }
    }
});
(uuay); O�0��X���p`��((p���*����*P���`���p���������������������( ��<`��\P�����������0�����0��LP�������`��0��D��������������P�����4���\P��������������������4p��\0�����������0������(���L���t@����������������D ��t@��� ���p�� ���  ���@ 0��` p��� ���� ���� ��� ���!���,!��L!���x!����!���!����!���"0��D"p��t"����"����"���#p��(# ��L#`��p#����#P���#����#p��$���D$���`$0��|$P���$����$����$��%���0%���X%����%`���% ���%���@&���d&���&����&���&P���&P�4'�\'���'���' �(��@(0	�h(
��(�
��(��(p�)��$)��T)@�x)0��)��)���) � *��H*@�p*���*��*��+��$+��D+�X+0�x+���+���+p��+ �,��@,�p,���,P ��,p ��,� �- !� -�!�H-"�p-`"��-�"��-P#��-�#�.0$�,.`$�L.�$�l.�$��.0%��.�%��. &�/�&�</P'�l/�'��/0(��/�(��/�(�0�(� 0)�@0 )�T0�)�|00*��0+��0�+��0p-� 10.�D1P1�t12��1�2��1�2��1P3�2p3�24�D2P4�d2p4�x2�4��2�5��2�5��2`6�(37�X3`7�|3�7��3:��3�:�4;�,40;�L4�;�t4<��4>��4p>��4�?�5�?�$5`@�P5�A��50B��5C��5`C��5D�$6pD�L6�D�`6�D�t6@E��60F��6@G�7PG�7�G�D7�G�X7pH��7�H��70I��7�I�8J�$8@K�d8PL��8`L��8 M��8@M��8�M�9�N�<9 O�d9�O��90P��9�P��9 Q�:�Q�<:�Q�\: R��:�R��:0S��:�S�;@T�8; U�l;V��;pV��;�V��;�W�<0X�<<`X�P<�Y��<�Y��<@Z��<�Z��< [�=�[�<= \�l=�\��=]��=�]��=�]�>`^�0>�^�T>P_��>�_��>@`��>�`��>a�?�a�@?�a�T?�b��?`e��?�e��?�f�@�f�<@ g�h@pk��@�n��@�o�A�o�A�p�XA�r��A�r��A�s��At�B�t�@BPu�lB`w��Bx��B0x��BPx�C�x�4CPy�`C�{��C@|��Cp~��C��D���@D���pD@���D`���D���D��E@��@E����E���8F���LF��`F��tF���F���F ���F@���FP���F����F��G��,G0��@G@��TGP��hG`��|Gp���G����G����G���G@���G���H���<H�PH��dH��xH0���Hp���H����H����H��I@��$I���LI���`I���I��I`���I0��J��4J���lJp���J0���J@���JP��Kp��<K��hK��|K���K����K ���K���L���DL��hL����L���L����L����L��M���@M��dM`���M����M���M`���M���N��0N���XN���|N`���N���N����NP��,O��PO��tO����O ���O`���O��$Pp��HP��lP����P���P���P ��(Q��LQp��pQ����Q����Q�Q0��Rp��DR���hR���|R0���R����R���S��(S���TS���S����S����S0��T���4TP��`T����T ���Tp���T����T���(U���XU����U@���U����U���V���(V ��PV����V0���Vp���V����V0��$WP��LW@��pW���W����W0��X���(X���LX0��lX`���X����X����X���X���Y ��@Y ��hYP���Y����Y����Y���Z���DZ0��tZ����Z���Z���Z ���Z0��[@��[P��,[ ��X[���[����[����[����[����[P��\`��,\p��@\���d\ ���\����\����\`��]��<] ��P]0��d]P��x]`���]����]����]���]���^��(^0��<^p��P^��x^���^p���^����^��_P��(_P���_���_���_`��$`���D`���X`���l`���`@���`����`��a ��a0��<a���ha����aP��a���a���aP�b��Lb���b���b��bP��b�c0�<c��lc���cp��c���c�
� d�
�@dP�dd���d��dp��d�� e �Te@�te���eP��ep�0f��lf���f��f��g��Lg���g�!��g "��g@"�h�"�4h�"�Th0$��hP%��h(�i�(�,i)�Ti�)�|i�)��i`*��i�*��i0+�j�+�@j�,�pj�,��j�,��j�-��jp.�k�.� k�.�@k�.�`k/�tk`/��kp/��k�/��k�/��k00�l@0�(l`0�<l�0�`l@1��l�1��l�3��l6�m`6�8m07�`m�7��m�7��m 8��m�8��m@9�n�9�@n@:�tn=��n`=��n�=��n>�o@A�8oPB�ho�B��o H��o�H��o`I�pPJ�@p�J�`p�J��p�J��p@K��p�K��p�L�qPM�Hq�N�xq�N��qO��q�O��q�P�rpQ�8r�Q�Xr�R�|r�S��rpY��r�Y��r�Z�$s�[�Ts�[�xs0\��sP\��s]��s�]�t�]�t�]�0t0^�Dt�^�Xt�^�|t�^��t_��t _��t`_��t�_�u�c�0ud�PuPd�pu�d��u�e��u@f��u0g�$v�g�Dvh�`v�h�|v�i��v�i��vPj��v k�wPn�Hw`o�pw@p��w�p��w�q�x r�$x�r�Hx�r�hxPs��x�s��x0t��x�t�y�t�$yy�Typ|��y�|��y�}��y��z@��Xz ���z����zp���z�� {���T{P���{����{���{���|0��8|���d|`���|����|`���|���}���4}��T}���|}@���}���}0��~@��,~���\~����~���~ ���~0��@��P��(`��<p��P���d���x����М��������0��������������$���8���X�0��l�P����������������Ԁ`���������<�`��\�P���������p������@��0����L���x�p����@������؂������4���d����������0������ ����T�P���� ��̄������� ����H���l� �������ȅP���������H����p� ����`������������$�@��D����l��������������ԇ������� ��`�@��t�����������@��܈ ���0��@�`��T�����������0��܉@�����8�`��d�����p��Ȋ���������(���X�����`�������� � ���H�zRx�$�6��4FJw�?9*3$"D�k�p\l��4t���������������0�С��̡�0�������(ܡ�7E�C
A�l H����E�C
A��$lȢ�fE�C
A�P
GE���
���&E�C
A�X���&E�C
A�X0�,���M�C
E���l
GOI�������%A�_
48���9A�C
I����
G2
Ns
E4pĥ�9A�C
I����
G2
Ns
E@�̧��W�C
D��E�E�D�|
�D�B�B�B�A�M`������@�X���W�C
D��E�E�D�B
�D�B�B�B�A�G`������@0���W�C
D��E�E�D�B
�D�B�B�B�A�G`������(tp���J�C
D��D�`K�����ԩ�>A�J
r���>A�J
r���>A�J
r4��>A�J
r$ T��E�C
G�����$H<���E�C
A�}
J pĬ�QE�C
A�m
J���.E�C
A�c���.E�C
A�c(� ��fE�C
C��n
GD
Ad��2E�J
E�]$ ����E�C
C��~
E$H���E�F
I���
E,p����E�F
I���C
IZ
A���,���FE�C
D��I�F�H�
H,�0��FE�C
D��I�F�H�
H P��`A�J
E�O$8����E�C
E���M
F$`���E�C
E���M
F ����=E�J
B�D�g ����=E�J
B�D�g,�Բ�E�Q
D��E�J���
C,ij�E�Q
D��E�J���
C,0���E�Q
F���E�H��
D,`���E�Q
F���E�H��
D(�����E�C
D��K��
A,�H��OE�C
B�I�G��K�$
A�h��8E�C
A�n���8E�C
A�n,���8E�C
A�nLȸ�:E�C
A�p l��}E�C
A�r �D���E�C
A��$����dE�C
C��o
I$���}E�C
E���J
E	`��	l��UE�L
A�~(8	����E�C
B�D�h
J�(d	P���E�C
B�D�h
J� �	��UE�C
C��E �	0��E�C
C��h,�	���?E�C
H����K�
E$
���fE�C
A�F
II,0
��?E�C
H����K�
E$`
��fE�C
A�F
II0�
<���A�C
B�G��E�D�p
JN,�
���E�C
H����I�s
L$�
����E�C
I���T
K @���E�U
A�� 8���=E�J
B�D�g\���MA�J
E�| |���E�C
A�p
G(����LE�C
B�D�\
F[4�����E�C
D��E�F��M
DD
D(P��	E�C
G����`
A04��Vr�_
Lx��Hd�_
h���$|����E�C
B�L�j
H(�0���E�F
M������
G ����^E�C
A�|
K$���nE�C
B�D�C
O$
H��PE�C
E�m
FO0D
p���A�E
D��E�E�O�x
EO4x
����E�C
B�E�E�O�D�q
H
O,�
����A�E
D��H���o
GH�
$���P�C
E���[
�B�B�A�HH�B�B�A�[����] ,����E�C
A��PD��MA�J
E�|pt��zY�C
Y�$�����A�O
C��r
A,�<��5A�C
B�G��D��
B4�L���
E�C
B�Q����
G�
G$ ���E�C
C��R
C,H����E�E
C��Z
IG
I0xL���E�C
M������
H�
H<����rP�C
B�E�F��s
�E�B�B�A�KHA�����$����A�G
A��
DG0����$,���ME�C
A�\
K\(T����A�E
A��
HE
C �d���E�C
A�m
J$����rQ�C
A�_
Dy �(��XE�M
U
Ig�d��>E�C
A�p,���:E�C
B�O���
GZ
N @���OE�C
A�D,d����E�C
B�N����s
E$�����E�C
C��k
J$�(���a�C
A�c
Hr$�����a�C
A�c
Hr$����a�C
A�c
Hr$4`���a�C
A�c
Hr$\���4E�C
A�
E$�����a�C
A�c
Hr$�H���a�C
A�c
Hr�����l���������3E�C
A�b0���	D���#E�J
A�R d����E�C
A�x
O �<���E�C
A��
H(�����a�C
C��b
G],�|���E�F
H����K�~
A ����A�J
E��
A,,���E�G
H����K�>
C \����A�J
E��
A(�$���E�C
E���M
Fa����(����YE�E
B�E�E�D�~����AE�C
E�p$����E�E
B�E�D�k$4`��WE�E
B�E�D�A\���AE�C
E�p$|����E�E
B�E�D�k$�0��WE�E
B�E�D�A �h��JE�C
B�G�x$�����E�E
B�E�D�o���.E�L
A�T8��.E�L
A�TX��0E�L
A�V$x,��ma�C
A�c
H\,�t���E�C
B�E�E�E�D�t$���FE�C
B�E�H�n,����E�C
B�E�E�E�D�j,(l���E�C
B�E�E�E�D�t$X��FE�C
B�E�H�n,����E�C
B�E�E�E�D�j$�d��ma�C
A�c
H\����E�C
H�������3E�C
A�b,��	$@��XE�J
D��D�@ h����E�C
A�x
O �����E�C
A��
H(�D���a�C
C��b
G],����E�F
H����K�~
A H���A�J
E��
A,0��E�G
H����K�>
C `���A�J
E��
A(�p��E�C
E���M
Fa���(����E�C
E���M
Fa�D�(P��E�C
E���M
Fa0��GE�L
p
AP��$d��ma�C
A�c
H\,�8��E�C
B�E�E�E�D�t$���FE�C
B�E�H�n,����E�C
B�E�E�E�D�j,0��E�C
B�E�E�E�D�t D��ZE�S
A�@$h��FE�C
B�E�H�n,��TE�F
H����H�-
H,�4��E�C
B�E�E�E�D�j$���ma�C
A�c
H\��E�C
H$8��ma�C
A�c
H\$`$�ma�C
A�c
H\$�l��E�C
A��
J �4	�kE�C
A�|
E$��	�JE�C
B�D��
B��
� (�
�sA�C
D��K�j
Cr,<�,A�C
D��D�
E{
E$l��E�C
D��D��$����E�M
B�D�z
F$�(
�XE�C
B�F��C(�`
��E�C
A�D
CE
K$�
�fY�G
a
GT
�E8�L(�,`$��E�C
B�E�H�Z
G\0����E�C
F���G�^
ML
D,�P�Y�J
E����
JO
A�0�$,�jE�F
B�D�P
G0t�#0D��|E�C
B�D�A
AF
JXx��0E�C
A�V$����E�C
E���s,�T��E�C
B�J��d
HX���3Y�C
HL�< ��-E�C
D��E
OO
IS
ET
L|0P ��E�C
B�E�D��
Aa
G� ��	,� ���E�C
D��F��j
Dw� <�$� H�ma�C
A�c
H\ !���E�C
A��
C$(!<��E�C
B�G�G
H(P!���E�C
D��J�E�p
A$|!(�pE�C
I���^$�!p�ma�C
A�c
H\0�!��yE�C
B�E�H�E
DQ
A$"�ma�C
A�c
H\("L�E�C
H H"L�dE�C
A�x
G0l"���E�C
B�D�Z
HT
LD$�"�gE�C
A�z
E^0�"L��E�C
B�F�a
OT
LD$�"��`E�C
A�u
B_0$#��E�C
B�D�a
IL
LD8X#���E�C
B�M��e
LT
LR
F^$�#P�ma�C
A�c
H\$�#��ma�C
A�c
H\ �#��E�C
A��
K$��?Y�S
R($��!8<$�$E�C
F���D�C
K�
LX
Hx$��:E�C
A�l �$�iE�C
A�V
E �$h�iE�C
A�V
E �$��iE�C
A�V
E %�iE�C
A�V
E,(%L��E�C
B�E�E�D�f
E X%��iE�C
A�V
E(|%��|E�C
B�E�D�_
E �%L�iE�C
A�V
E �%��^E�C
L
E(�%��~E�C
B�E�D�a
E &( �^E�C
L
E,@&d ��E�C
B�E�E�D�f
E(p&� �~E�C
B�E�D�a
E �&!�iE�C
A�V
E �&d!�^E�C
L
E �&�!�iE�C
A�V
E '�!�iE�C
A�V
E,'8"�
,@'4"�E�C
G��{
Fl
A4p'#��E�C
F���E�D�A
H�
F,�'�%��E�C
G����T
Ef,�'�%��E�C
E���_
Dn
B(�&�E�C
H(((�&�NE�C
B�E�E�D�q4T(�&�IE�C
D��E�E�D��
Mt
L,�(�*�E�C
B�E�E�E�D��
A4�(�-�XE�C
F���E�D�
Jh
H�(�.�8)�.��E�C
G����o
J`
HX
HS,D)�/��E�F
E���^
BZ
F(t)�0�oE�C
D��D�n
Bn,�)41��E�C
D��H�b
JG,�)�1��E�C
F���E�H�^
G(*�1��E�C
M�����T
G(,*�2�SJ�C
A�d
��NGI��(X*�2�E�C
I������(�*�4��E�G
D��I�H���*D5�E�C
F�*D5�E�C
F,�*D5��J�C
A�A
��IUC��( +�5�nE�C
E�c
HP
E0L+�5��E�C
F���D�f
H�
N �+D8�QE�C
B�D�B,�+�8�'E�C
I����
OZ
F$�+�:�E�C
C���
F,�+h;�E�C
D��D�V
Jq
G,,,X<��E�L
F���E�R��
A,\,?��E�C
B�E�H��E��
A(�,�?�E�C
E����
H(�,�A��E�J
C��b
DR�,�A�E�C
F$-�A�$E�C
C���
K�,-�F�dE�C
F�Q
Ib
Fa
Ob
Na
Oa
Ob
Nb
Nb
N^
J^
J[
E[
Ep
HG
I_
I_
I_
I_
IP�-�J��E�C
B�E�H�y
HE
Cn
JB
FG
IX$.@L�8.<L�PL.xL�`.tL�t.pL��.lL��.hL��.tL��.pL�CE�C
A�x�.�L�;E�C
A�p/�L�/�L�#,/�L�@/�L�T/�L�h/�L�|/�L��/�L� �/�L�dE�C
A�Y�/,M�&E�C
A�X�/<M�7E�C
A�l0\M�;E�C
A�p(0|M�<0xM�P0�M�d0�M� x0�M�;E�C
B�D�h�0�M��0�M�)E�Q
R�0�M�JE�C
A�|�0N�+Q�Q
H$1N�_Q�E
A�a
H^81LN�(L1HN�,A�C
A��
IZ
F$x1LO��E�M
G��H
G(�1P��Y�C
I���ZA����$�1xP��E�C
B�H�N
H(�1 Q��E�C
B�E�H��
G4 2�Q��E�C
B�E�E�E�D��
KN
B4X2S��E�C
B�E�D��
LJ
FZ
F$�2�S��E�C
B�D�U
E�2\T�(�2XT�A�C
F���E�N��,�2<U�A�C
D��S����
C((3,V�qE�C
B�D�{
OTT3�V�(h3�V��a�E
C���
�B�A�I(�30W��a�E
C���
�B�A�I �3�W�aE�C
A�F
A �3 X�YE�C
A�{
D$4\X�<E�C
A�[
DO 04tX�AE�C
A�f
A T4�X��E�C
A�s
D x4Y�IE�C
A�i
F �4HY�yE�C
A�Z
E�4�Y�2E�H
E�[�4�Y�,E�H
A�Y(5�Y��E�C
G����S
N ,5XZ�fA�J
E�UP5�Z�MA�J
E�|p5�Z�>A�J
r �5�Z�`A�J
E�O �50[�`A�J
E�O �5l[�`A�J
E�O�5�[�>A�J
r$6�[��E�C
E���j
I D6`\��E�C
A��
G$h6�\��E�C
C���
E(�6�_��J�C
D��D�`K����(�6`��J�C
D��D�`K����,�6|`��E�C
D��H�c
IH 7a��A�J
E��
A <7�a��A�C
E��
E0`7db��E�C
B�E�G�t
Fa
Ga,�7�b��E�C
B�E�G�r
Hn
B�7@c�8E�C
A�j(�7`c��E�C
D��E�E�D�� 8�c�[E�C
A�P 48 d�fA�J
E�U,X8ld��A�C
B�E�G��D��
H0�8�e�hA�C
M������
Eo(�8g�A�C
B�J����
E(�8h�E�C
M�����[
H 9�i��E�C
A��
I 89�j�YE�H
B�D�A\9�j�MA�J
E�|,|9(k��A�C
Y�����|
G�9�l�#E�J
M0�9�l�UE�C
B�E�G��M��
Kp,:n�<A�C
F���D��
GW 0:$o�6E�F
P
EUT:@o�$h:<o�nE�C
A�S
LE0�:�o��I�G
B�E�E�E�D��G������(�:p��E�C
D��F�N�t
D �:�q�qE�C
A�O
H(;�q��E�K
B�D�\
ND,@;Dr�nE�C
M�����
K$p;�s��E�C
B�K�R
I4�;�s�+E�C
B�E�G��D��
KN
B�;�v�JE�C
A�,�;$w�aE�C
B�J��J���
H( <dx��E�C
I�����n
AL<�x�MA�J
E�|$l<y�tE�C
B�H�\
A�<py�MA�J
E�|,�<�y�DE�C
D��H���*
A,�<�z��E�C
D��I�E�H��
A,=P{�DE�C
D��H���*
A,D=p|��E�C
D��I�E�H��
A(t=}��E�C
D��D��
A�=�}�MA�J
E�|0�=�}��E�C
B�E�D��
FW
I�=p~�MA�J
E�|$>�~�DE�C
A�!
FH<>���P�C
E���X
�B�B�A�KI�B�B�A�Z����]�>L��;E�C
A�q�>l��;E�C
A�q�>���MA�J
E�|$�>���mE�C
A�p
Gh$?��E�C
C���
K 8?����E�C
A��
O0\?Ȃ��E�C
B�J��d
H`
HU(�?T���J�C
D��D�`K����,�?����E�C
D��H�h
DR$�?(��VE�C
A�y
NE @`��WE�C
A�L8@���6E�C
A�kX@���.E�C
A�cx@̄�4E�G
E�b�@��,�@��SE�C
B�H�F�E�H��
G �@���E�C
A��
M(A���ME�F
C��n
LD$,A؆��E�C
B�J��|
H(TA���-E�C
B�N���
E,�A���mA�C
B�J��M��V
E(�A���E�C
D��F�D��
A �A����E�C
A��
F,B���6E�C
F���E�H��
I,0B���CE�C
D��F�K�� 
A `B���fA�J
E�U,�B��RA�G
D��I�F�E�
E�B0���B,��	�B(��	�B$��	C ��	(C���A�C
B�G��D��
A,DC����E�C
B�J��J��u
M$tCp��rE�C
B�K���X�CȒ�	�CĒ�	�C���(�C̒��E�C
B�E�D�n
AD0��D,�� ,D(��&E�G
J
JA(PD4��}E�C
B�_���
K,|D����E�C
B�G��E�H�r
H�D��0�D��O�C
A�aD��X��j��0�DP���E�C
B�E�E�D�J
Fm(E���<Eȕ�PEĕ�dEЕ�xE̕��Eؕ�$�E��ma�C
A�c
H\4�E,���E�C
G����P
I}
KXFĖ�FЖ�%(F��3$<F��ma�C
A�c
H\dF`��E�C
H$�F`��ma�C
A�c
H\�F���E�C
H �F���YE�C
A�v
A �F��YE�C
A�v
A`G ���E�C
F�n
LT
LP
Hf
JK
EK
EK
EO
IL
Ld,xG����E�C
E����
Ha
G<�GL���E�C
C��`
ET
LK
EG
Ia$�G��ma�C
A�c
H\H4��E�C
H0H4��DH0��XH<��$lHH��ma�C
A�c
H\�H���6Y�C
R4�H����W�H
A�Z
Fe��S��L���H��	$I��E�C
E����
A((I��ME�J
C��k
KDTI��7Y�C
Z(tI0���Y�C
E�f��Q���I���7Y�C
Z �I���SE�C
C��G �I��TE�C
C��H,J,��iE�C
D��M��|
KH08Jl���E�C
E���Z
I_
I`lJ���1E�C
A�g�J��3E�C
A�i(�J8��DE�C
B�E�E�D�g,�J\���E�C
H����H�V
BK��E�C
K,(K��aE�C
M�����c
H,XK,���E�C
B�G�L����
J,�Kܧ��E�H
D��I�J�D�[
E$�K���ma�C
A�c
H\(�K���E�C
D��I�H�~
EL���E�C
H ,L���^E�C
A�TPL��*E�C
A�_,pL��yE�C
B�D�y
IT
L$�LD��ma�C
A�c
H\@�L���YE�C
F���E�D��
IN
J^
J�
I0M���GE�C
B�E�D��
Ga
G@Mĭ�E�C
H$`Mĭ�ma�C
A�c
H\H�M���E�C
H����D��
K^
J�
C[
EQ
OD�M`��E�C
D��E�E�D��
B^
J�
JZ
F8N8��DE�C
B�E�D��
NN
JZ
N<XNL��,E�C
B�E�E�E�D��
HN
B^
J0�N<��E�C
B�E�E�D��
G_
I4�N(���E�C
D��E�E�D��
H}
K0O���E�C
B�E�E�D��
G_
I08Ol��E�C
B�E�E�D��
G_
I4lOX���E�C
B�E�E�D��
G�
I0�O����E�C
B�D�F
L[
E[�O,��E�C
H$�O,��ma�C
A�c
H\ Pt��E�C
H4@Pt��YE�C
D��D�	
GZ
FJ
F4xP���E�C
D��D��
JZ
FJ
F@�P����E�F
C���
Jg
Iv
JV
J�
I �P���E�C
A�K
L$Ql��ma�C
A�c
H\$@Q���ma�C
A�c
H\$hQ���ma�C
A�c
H\$�QD��ma�C
A�c
H\�Q���E�C
H,�Q����E�C
B�I�G�`
Fz
F R��E�H
E�k
k,,Rh���E�C
D��O�t
A@\R��$pR��BE�C
B�E�G�g$�R<���E�C
B�D�Z
H4�R����E�C
B�E�D�d
Ia
Ga�R\��Sh��-E�M
Z,Sx��,E�L
VLS���	$`S���UE�E
B�E�D�{�S���	$�S���UE�E
B�E�D�{�S���	$�S���OE�E
B�E�D�uT��	T�� (T��dE�C
A�U
A$LTh��ma�C
A�c
H\$tT���ma�C
A�c
H\,�T���E�C
H����D��
B0�T���:E�C
H����D��
Dq
O U���]E�C
A�n
E$$U ���E�C
E����
A$LU���ma�C
A�c
H\tU��BY�C
A�d�U@��/E�C
E�a(�UP���E�C
C��x
M[$�U���bE�C
A�v
Aa V��YE�C
A�v
A0,VX���E�C
B�D�}
Ec
MT0`V����E�C
F���D�P
N!
O�V`��BY�C
A�d�V���BY�C
A�d�V���BY�C
A�d,�V���2A�C
\������
A,$W��E�C
B�E�E�D�N
JTW���)E�C
E�[4tW����E�C
B�G��E�D��
Hq
O �WX��ZE�C
A�x
G(�W����E�C
C���
JH,�WH���E�C
B�J�����
A,X��-E�C
dLX��E�F
MlX��"E�C
Y$�X(��cE�C
B�H�K
A$�Xp��cE�C
B�F�M
A$�X����E�C
B�D��
A,Y`���E�C
D��E�D�q
J{,4Y��1E�C
D��H��
Fd
DdY��
 xY��QO�C
A�o��(�YH���E�C
C��T
I�0�Y���E�C
H����D��
FN$�Y����E�Q
D��D�f$Z0��%E�C
A�T DZ@��E�C
A��
E0hZ<��E�C
D��H��
Gl
LJ4�Z(���A�F
F���E�H�W
O�
F�Z���$�Z|��"E�C
D��H�	,[����A�C
D��D��
KJ @[T��3E�F
B�G�Z d[p��3E�F
B�G�Z�[���,�[����E�C
D��H�K
IX
A$�[��uE�F
D��F��[�[p��\l��\h���0\���G D\ ��QN�C
A�w��h\\��|\X���\d��$�\`��<E�C
A�[
DO �\x���E�C
A�_
H(�\����E�C
G�����
A]���>A�J
r<]���>A�J
r\]���>A�J
r$|]����E�C
C���
H0�]����N�C
E���h
BM�B�B�A�4�]L���E�C
B�J��t
H]
Cf
J^��MA�J
E�|0^4��|Z�]
L^���yW�]
 h^����A�F
t
m�^���Vt�]
�^��kI�]
<�^p���A�F
C
FJ
FJ
FJ
FC
A,_��0A�C
D��J�I�H�?
A$4_��E�C
C���
A,\_���E�C
D��F�G���
A,�_���wE�C
B�E�E�E�M�L,�_���E�C
B�E�J�E�H��
A �_���`A�J
E�O `��fA�J
E�U4`@��EE�J
E�p T`p��pE�C
A�b$x`���|A�C
E���n(�`��UE�E
C��v
MD �`H��eE�C
E�o
D�`���BE�C
A�t,a��E�E
F���T���
D4@a���fE�C
B�E�E�Q���
J�
E(xa��dE�F
B�F�^
O_ �a ��E�C
A��
K,�a��mA�C
B�E�G�H�D�X
JH�a��$N�C
I�����Z
Dy�B�B�B�B�A�G������HDb���Q�C
B�E�E�D�Y
�K�E�E�A�MT
�O�B�B�A�HH,�bt�{E�F
B�E�N�H
HJ$�b���E�F
I����
A �bl��E�C
A�s
D0c��qA�C
M������
J^
A(@c$
��A�C
M������
E,lc�
�$A�C
B�E�E�J���
E$�c��TE�C
A�)
N,�c
��E�C
B�J��i
CV,�cp
��E�C
D��F��r
DY($d�
��J�C
D��D�eF����,PdT��E�C
D��H�m
GQ �d��\E�E
A�P$�d��E�C
B�J��$�dx�IA�C
C��<
A(�d��
A�C
G�����
D e��CE�C
A�x$@e���A�C
E�y
F\$he<�wE�C
B�D�S
O(�e���A�C
D��I��
A,�e8�E�C
H����H�c
E(�e(�A�C
B�J���
F,f�;A�C
D��E�E�K��
DPHf�E�C
B�G��H��
I�
No
I�
Gv
J�
K$�f��ma�C
A�c
H\�f ��f,�
�f(�
g$�
g �
(g�
<g�
Pg�
dg�xg��g(��g4��g@��gL�IE�C
{
A�g|��g��h��$h��*E�Q
SDh��Xh��lh��HE�H
u
A�h���h�IE�H
v
A�h4�IE�H
v
A�hd�+E�C
A�a$it�ma�C
A�c
H\(i��[E�C
T
A,Hi���A�C
M������
E,xi� �nA�C
M�����R
E,�i�&��E�C
P�����j
A �i|<��E�O
y
C�i�<�)E�I
Zj=�Hf�]
(8j<=�yE�C
C��}
HI
Adj�=�Vt�]
�j�=����]
$�j�>��E�C
E���T
G(�j ?��E�C
K�����
E,�j�A�0E�C
M������
F, k�D��E�C
D��E�E�D�
C Pk�F��E�C
�
C$tkH�ma�C
A�c
H\8�kXH�E�C
B�E�E�D��
FH
HX
H0�k<I��E�C
B�E�E�D�X
HX
H0l�I��E�C
B�E�E�D�X
HX
H<@l4J��E�C
D��G��D�`
It
Lq
O4�l�L��E�C
B�E�D�d
Ia
Ga$�lLM�ma�C
A�c
H\(�l�M��E�C
B�E�D��
F$mXO��E�C
B�D��
E 4m0P��E�C
A�P
G0Xm�P�A�C
F���D��
IJ
E$�mxQ��E�C
D��H�� �m R�YE�C
A�v
A,�m\R��E�C
B�F��p
He(n�R�tE�C
B�D�A
I[$4n0S�ma�C
A�c
H\\nxS�0E�C
A�V(|n�S�5E�C
G�����
E<�n�V� E�C
B�E�E�E�D�w
L~
Bc
M$�n|W�ma�C
A�c
H\o�W�MA�J
E�|$0o�W�ma�C
A�c
H\(Xo<X�E�C
D��D��
H$�o0Y��E�C
A��
J�o�Z��o�Z� �o�Z�>E�C
B�D�oP�o�Z�E�C
H����D�o
E�
Lo
I�
L�
EDLp�^�$`p�^�ZE�C
E�a
Bi�p�^�(�p�^��E�C
G����K
F4�p\`��E�C
F���E�D�c
Fr
N(qb�E�C
D��H��
G,q�b�#<@qc�AE�C
B�J��M���
Fy
Oy
G�qf�)0�q0f�HE�C
I���X
GJ
F,�qLh�E�C
M������
C(�q,k��E�C
D��E�D�n
E($r�k��E�C
D��E�D�k
H4Pr�k�bE�C
D��S����
EY
A(�r,m��E�C
G����x
I�r�m�&E�C
V�r�m�AE�C
A�b�r�m�E�C
F,s�m�E�C
D��H��D��
A,Ds�o��E�C
B�G��G��>
J,ts�s�ME�\
H��F�E�L��
G$�s�t�aE�C
E���O�s�t�'E�C
A�Y�s�t�E�C
F$t�t�zE�C
C��S
B4tPu�^@����p������1�1�1�����1r9�1����111�1����-=9=����;=�1����I=S=����Y4R6����;=�1����W=MG����\=�1����;=�1 �,�,��,`�,��,`�, �,�,����c5 �,�,1����P5@�,1����75`�,�0�,��,��,�&�(�+&P��8a=i=r=z=�=�=�=x`0O�� ��,��,`�,����M1 �,�=�,�<�������,111x6����31@�,��,�=�=�=�`�=�`>>*>�`1>B>|>a^>Hav>pa�>�>5C�>�>�>�>�>??;?C?[?c?<9|?E9�?�?�?�?�?�?�?@�a@�a @ �a-@@�a:@�H@`@����f@|@(xM�M^M�v�v�v�v�vww(w>wHw]wiw�w�w�w�w�w�w�wxx'x9xHx`xmxwx�xp
#@�,P�,���o���8
y��,PO���	���o���o����o�o�����o�0�,0p@pPp`ppp�p�p�p�p�p�p�p�pqq q0q@qPq`qpq�q�q�q�q�q�q�q�qrr r0r@rPr`rpr�r�r�r�r�r�r�r�rss s0s@sPs`sps�s�s�s�s�s�s�s�stt t0t@tPt`tpt�t�t�t�t�t�t�t�tuu u0u@uPu`upu�u�u�u�u�u�u�u�uvv v0v@vPv`vpv�v�v�v�v�v�v�v�vww w0w@wPw`wpw�w�w�w�w�w�w�w�wxx x0x@xPx`xpx�x�x�x�x�x�x�x�xyy y0y@yPy`ypy�y�y�y�y�y�y�y�yzz z0z@zPz`zpz�z�z�z�z�z�z�z�z{{ {0{@{P{`{p{�{�{�{�{�{�{�{�{|| |0|@|P|`|p|�|�|�|�|�|�|�|�|}} }0}@}P}`}p}�}�}�}�}�}�}�}�}~~ ~0~@~P~`~p~�~�~�~�~�~�~�~�~ 0@P`p���������� �0�@�P�`�p�����������Ѐ���� �0�@�P�`�p�����������Ё���� �0�@�P�`�p�����������Ђ���� �0�@�P�`�p�����������Ѓ���� �0�@�P�`�p�����������Є���� �0�@�P�`�p�����������Ѕ���� �0�@�P�`�p�����������І���� �0�@�P�`�p�����������Ї���� �0�@�P�`�p�����������Ј���� �0�@�P�`�p�����������Љ���� �0�@�P�`�p�����������Њ���� �0�@�P�`�p�����������Ћ���� �0�@�P�`�p�����������Ќ���� �0�@�P�`�p�����������Ѝ���� �0�@�P�`�p�����������Ў���� �0�@�P�`�p�����������Џ���� �0�@�P�`�p�����������А���� �0�@�P�`�p�����������Б���� �0�@�P�`�p�����������В���� �0�@�P�`�p�����������Г���� �0�@�P�`�p�����������Д���� �0�@�P�`�p�����������Е���� �0�@�P�`�p�����������Ж���� �0�@�P�`�p�����������З���� �0�@�P�`�p�����������И���� �0�@�P�`�p�����������Й���� �0�@�P�`�p�����������К���� �0�@�P�`�p�����������Л���� �0�@�P�`�p�����������М���� �0�@�P�`�p�����������Н���� �0�@�P�`�p�����������О���� �0�@�P�`�p�����������П���� �0�@�P�`�p�����������Р���� �0�@�P�`�p�����������С���� �0�@�P�`�p�����������Т���� �0�@�P�`�p�����������У���� �0�@�P�`�p�����������Ф����,��,p,���w�'/usr/lib/debug/.dwz/x86_64-linux-gnu/gnome-shell.debugUP�����
��F�nf7b319378692a41e52cfac07719a7f8177ac2f1.debug7�?�.shstrtab.note.gnu.property.note.gnu.build-id.gnu.hash.dynsym.dynstr.gnu.version.gnu.version_r.rela.dyn.rela.plt.init.plt.got.plt.sec.text.fini.rodata.gresource.shell_js_resources.eh_frame_hdr.eh_frame.init_array.fini_array.data.rel.ro.dynamic.data.bss.gnu_debugaltlink.gnu_debuglink�� ��$1���o��H;88HoC����yK���o����F	X���o���g���qB��PO{ppv p p�4���p������4�`�`��H�##
�00�h �����x�'�!,!,�9,9,Ht�@�,@�,�P�,P�,�`�,`�,� 0�,0�,����,��,0�,�,� ��,��,0 ��,K(�,4H�,7

Zerion Mini Shell 1.0