2022-01-02 01:46:44 -05:00
use cascade ::cascade ;
use gdk4 ::ContentProvider ;
use gdk4 ::Display ;
use gdk4 ::ModifierType ;
use gio ::DesktopAppInfo ;
use gio ::Icon ;
use glib ::Object ;
use glib ::Type ;
use gtk4 ::prelude ::ListModelExt ;
use gtk4 ::prelude ::* ;
use gtk4 ::subclass ::prelude ::* ;
use gtk4 ::DropTarget ;
use gtk4 ::IconTheme ;
use gtk4 ::ListView ;
use gtk4 ::Orientation ;
use gtk4 ::SignalListItemFactory ;
use gtk4 ::Window ;
use gtk4 ::{ gio , glib } ;
use gtk4 ::{ DragSource , GestureClick } ;
use std ::fs ::File ;
use std ::path ::Path ;
use crate ::dock_item ::DockItem ;
use crate ::dock_object ::DockObject ;
use crate ::utils ::data_path ;
use crate ::BoxedWindowList ;
use crate ::Event ;
use crate ::Item ;
use crate ::TX ;
mod imp ;
glib ::wrapper! {
pub struct DockList ( ObjectSubclass < imp ::DockList > )
@ extends gtk4 ::Widget , gtk4 ::Box ,
@ implements gtk4 ::Accessible , gtk4 ::Buildable , gtk4 ::ConstraintTarget , gtk4 ::Orientable ;
}
#[ derive(Debug, PartialEq, Copy, Clone) ]
pub enum DockListType {
Saved ,
Active ,
}
impl Default for DockListType {
fn default ( ) -> Self {
DockListType ::Active
}
}
impl DockList {
pub fn new ( type_ : DockListType ) -> Self {
let self_ : DockList = glib ::Object ::new ( & [ ] ) . expect ( " Failed to create DockList " ) ;
let imp = imp ::DockList ::from_instance ( & self_ ) ;
imp . type_ . set ( type_ ) . unwrap ( ) ;
self_ . layout ( ) ;
//dnd behavior is different for each type, as well as the data in the model
self_ . setup_model ( ) ;
self_ . setup_click_controller ( ) ;
self_ . setup_drag ( ) ;
self_ . setup_drop_target ( ) ;
self_ . setup_factory ( ) ;
self_
}
pub fn model ( & self ) -> & gio ::ListStore {
// Get state
let imp = imp ::DockList ::from_instance ( self ) ;
imp . model . get ( ) . expect ( " Could not get model " )
}
pub fn drop_controller ( & self ) -> & DropTarget {
// Get state
let imp = imp ::DockList ::from_instance ( self ) ;
imp . drop_controller . get ( ) . expect ( " Could not get model " )
}
2022-01-03 21:33:26 -05:00
pub fn popover_index ( & self ) -> Option < u32 > {
// Get state
let imp = imp ::DockList ::from_instance ( self ) ;
imp . popover_menu_index . get ( )
}
2022-01-02 01:46:44 -05:00
fn restore_data ( & self ) {
if let Ok ( file ) = File ::open ( data_path ( ) ) {
if let Ok ( data ) = serde_json ::from_reader ::< _ , Vec < String > > ( file ) {
// dbg!(&data);
let dock_objects : Vec < Object > = data
. into_iter ( )
. filter_map ( | d | {
DockObject ::from_app_info_path ( & d )
. map ( | dockobject | dockobject . upcast ::< Object > ( ) )
} )
. collect ( ) ;
// dbg!(&dock_objects);
let model = self . model ( ) ;
model . splice ( model . n_items ( ) , 0 , & dock_objects ) ;
return ;
}
}
println! ( " Error loading saved apps! " ) ;
let model = & self . model ( ) ;
xdg ::BaseDirectories ::new ( )
. expect ( " could not access XDG Base directory " )
. get_data_dirs ( )
. iter_mut ( )
. for_each ( | xdg_data_path | {
let defaults = [ " Firefox Web Browser " , " Files " , " Terminal " , " Pop!_Shop " ] ;
xdg_data_path . push ( " applications " ) ;
// dbg!(&xdg_data_path);
if let Ok ( dir_iter ) = std ::fs ::read_dir ( xdg_data_path ) {
dir_iter . for_each ( | dir_entry | {
if let Ok ( dir_entry ) = dir_entry {
if let Some ( path ) = dir_entry . path ( ) . file_name ( ) {
if let Some ( path ) = path . to_str ( ) {
if let Some ( app_info ) = gio ::DesktopAppInfo ::new ( path ) {
if app_info . should_show ( )
& & defaults . contains ( & app_info . name ( ) . as_str ( ) )
{
model . append ( & DockObject ::new ( app_info ) ) ;
} else {
// println!("Ignoring {}", path);
}
} else {
// println!("error loading {}", path);
}
}
}
}
} )
}
} ) ;
}
fn store_data ( model : & gio ::ListStore ) {
// Store todo data in vector
let mut backup_data = Vec ::new ( ) ;
let mut i = 0 ;
while let Some ( item ) = model . item ( i ) {
// Get `AppGroup` from `glib::Object`
let dock_object = item
. downcast_ref ::< DockObject > ( )
. expect ( " The object needs to be of type `AppGroupData`. " ) ;
// Add todo data to vector and increase position
if let Ok ( Some ( app_info ) ) = dock_object
. property ( " appinfo " )
. expect ( " DockObject must have appinfo property " )
. get ::< Option < DesktopAppInfo > > ( )
{
if let Some ( f ) = app_info . filename ( ) {
backup_data . push ( f ) ;
}
}
i + = 1 ;
}
// dbg!(&backup_data);
// Save state in file
let file = File ::create ( data_path ( ) ) . expect ( " Could not create json file. " ) ;
serde_json ::to_writer_pretty ( file , & backup_data )
. expect ( " Could not write data to json file " ) ;
}
fn layout ( & self ) {
let imp = imp ::DockList ::from_instance ( & self ) ;
let list_view = cascade! {
ListView ::default ( ) ;
.. set_orientation ( Orientation ::Horizontal ) ;
2022-01-05 15:37:04 -05:00
.. add_css_class ( " docklist " ) ;
2022-01-02 01:46:44 -05:00
} ;
if imp . type_ . get ( ) . unwrap ( ) = = & DockListType ::Saved {
list_view . set_width_request ( 64 ) ;
}
self . append ( & list_view ) ;
imp . list_view . set ( list_view ) . unwrap ( ) ;
}
fn setup_model ( & self ) {
let imp = imp ::DockList ::from_instance ( self ) ;
let model = gio ::ListStore ::new ( DockObject ::static_type ( ) ) ;
let selection_model = gtk4 ::NoSelection ::new ( Some ( & model ) ) ;
// Wrap model with selection and pass it to the list view
let list_view = imp . list_view . get ( ) . unwrap ( ) ;
list_view . set_model ( Some ( & selection_model ) ) ;
imp . model . set ( model ) . expect ( " Could not set model " ) ;
if imp . type_ . get ( ) . unwrap ( ) = = & DockListType ::Saved {
let model = self . model ( ) ;
self . restore_data ( ) ;
model . connect_items_changed ( | model , _ , _removed , _added | {
Self ::store_data ( & model ) ;
} ) ;
}
}
fn setup_click_controller ( & self ) {
let imp = imp ::DockList ::from_instance ( self ) ;
let controller = GestureClick ::builder ( )
. button ( 0 )
. propagation_limit ( gtk4 ::PropagationLimit ::None )
. propagation_phase ( gtk4 ::PropagationPhase ::Capture )
. build ( ) ;
self . add_controller ( & controller ) ;
let model = self . model ( ) ;
2022-01-03 21:33:26 -05:00
let list_view = & imp . list_view . get ( ) . unwrap ( ) ;
let popover_menu_index = & imp . popover_menu_index ;
controller . connect_released ( glib ::clone! ( @ weak model , @ weak list_view , @ weak popover_menu_index = > move | self_ , _ , x , y | {
2022-01-02 01:46:44 -05:00
let window = list_view . root ( ) . unwrap ( ) . downcast ::< Window > ( ) . unwrap ( ) ;
let max_x = list_view . allocated_width ( ) ;
2022-01-03 21:33:26 -05:00
let max_y = list_view . allocated_height ( ) ;
2022-01-05 15:37:04 -05:00
// dbg!(max_y);
// dbg!(y);
2022-01-02 01:46:44 -05:00
let n_buckets = model . n_items ( ) ;
let index = ( x * n_buckets as f64 / ( max_x as f64 + 0.1 ) ) as u32 ;
2022-01-05 15:37:04 -05:00
// dbg!(self_.current_button());
// dbg!(self_.last_event(self_.current_sequence().as_ref()));
2022-01-02 01:46:44 -05:00
let click_modifier = if let Some ( event ) = self_ . last_event ( self_ . current_sequence ( ) . as_ref ( ) ) {
2022-01-05 15:37:04 -05:00
// dbg!(&event);
2022-01-02 01:46:44 -05:00
Some ( event . modifier_state ( ) )
}
else {
None
} ;
2022-01-05 15:37:04 -05:00
// dbg!(click_modifier);
2022-01-02 01:46:44 -05:00
// Launch the application when an item of the list is activated
let focus_window = move | first_focused_item : & Item | {
let entity = first_focused_item . entity . clone ( ) ;
glib ::MainContext ::default ( ) . spawn_local ( async move {
if let Some ( tx ) = TX . get ( ) {
let _ = tx . send ( Event ::Activate ( entity ) ) . await ;
}
} ) ;
} ;
2022-01-03 21:33:26 -05:00
let old_index = popover_menu_index . get ( ) ;
if let Some ( old_index ) = old_index {
if let Some ( old_item ) = model . item ( old_index ) {
if let Ok ( old_dock_object ) = old_item . downcast ::< DockObject > ( ) {
println! ( " removing popup... " ) ;
old_dock_object . set_popover ( false ) ;
popover_menu_index . replace ( None ) ;
model . items_changed ( old_index , 0 , 0 ) ;
}
}
return ;
}
if y > f64 ::from ( max_y ) | | y < 0.0 | | x > f64 ::from ( max_x ) | | x < 0.0 {
println! ( " out of bounds click... " ) ;
return ;
}
2022-01-02 01:46:44 -05:00
if let Some ( item ) = model . item ( index ) {
if let Ok ( dock_object ) = item . downcast ::< DockObject > ( ) {
let active = dock_object . property ( " active " ) . expect ( " DockObject must have active property " ) . get ::< BoxedWindowList > ( ) . expect ( " Failed to convert value to WindowList " ) ;
let app_info = dock_object . property ( " appinfo " ) . expect ( " DockObject must have appinfo property " ) . get ::< Option < DesktopAppInfo > > ( ) . expect ( " Failed to convert value to DesktopAppInfo " ) ;
match ( self_ . current_button ( ) , click_modifier , active . 0. iter ( ) . next ( ) , app_info ) {
( click , Some ( click_modifier ) , Some ( first_focused_item ) , _ ) if click = = 1 & & ! click_modifier . contains ( ModifierType ::CONTROL_MASK ) = > focus_window ( first_focused_item ) ,
( click , None , Some ( first_focused_item ) , _ ) if click = = 1 = > focus_window ( first_focused_item ) ,
( click , _ , _ , Some ( app_info ) ) | ( click , _ , None , Some ( app_info ) ) if click ! = 3 = > {
let context = window . display ( ) . app_launch_context ( ) ;
if let Err ( err ) = app_info . launch ( & [ ] , Some ( & context ) ) {
gtk4 ::MessageDialog ::builder ( )
. text ( & format! ( " Failed to start {} " , app_info . name ( ) ) )
. secondary_text ( & err . to_string ( ) )
. message_type ( gtk4 ::MessageType ::Error )
. modal ( true )
. transient_for ( & window )
. build ( )
. show ( ) ;
}
}
( click , _ , _ , _ ) if click = = 3 = > {
println! ( " handling right click " ) ;
2022-01-03 21:33:26 -05:00
if let Some ( old_index ) = popover_menu_index . get ( ) . clone ( ) {
if let Some ( item ) = model . item ( old_index ) {
if let Ok ( dock_object ) = item . downcast ::< DockObject > ( ) {
dock_object . set_popover ( false ) ;
popover_menu_index . replace ( Some ( index ) ) ;
model . items_changed ( old_index , 0 , 0 ) ;
}
}
}
dock_object . set_popover ( true ) ;
popover_menu_index . replace ( Some ( index ) ) ;
model . items_changed ( index , 0 , 0 ) ;
2022-01-02 01:46:44 -05:00
}
_ = > println! ( " Failed to process click. " )
}
}
}
} ) ) ;
imp . click_controller . set ( controller ) . unwrap ( ) ;
}
fn setup_drop_target ( & self ) {
let imp = imp ::DockList ::from_instance ( self ) ;
if imp . type_ . get ( ) . unwrap ( ) ! = & DockListType ::Saved {
return ;
}
let drop_target_widget = & imp . list_view . get ( ) . unwrap ( ) ;
let mut drop_actions = gdk4 ::DragAction ::COPY ;
drop_actions . insert ( gdk4 ::DragAction ::MOVE ) ;
let drop_format = gdk4 ::ContentFormats ::for_type ( Type ::STRING ) ;
let drop_format = drop_format
. union ( & gdk4 ::ContentFormats ::for_type ( Type ::U32 ) )
. expect ( " couldn't make union " ) ;
let drop_controller = DropTarget ::builder ( )
. preload ( true )
. actions ( drop_actions )
. formats ( & drop_format )
. build ( ) ;
drop_target_widget . add_controller ( & drop_controller ) ;
let model = self . model ( ) ;
let list_view = & imp . list_view . get ( ) . unwrap ( ) ;
let drag_end = & imp . drag_end_signal ;
let drag_source = & imp . drag_source . get ( ) . unwrap ( ) ;
drop_controller . connect_drop (
glib ::clone! ( @ weak model , @ weak list_view , @ weak drag_end , @ weak drag_source = > @ default - return true , move | _self , drop_value , x , _y | {
//calculate insertion location
let max_x = list_view . allocated_width ( ) ;
let n_buckets = model . n_items ( ) * 2 ;
let drop_bucket = ( x * n_buckets as f64 / ( max_x as f64 + 0.1 ) ) as u32 ;
let index = if drop_bucket = = 0 {
0
} else if drop_bucket = = n_buckets - 1 {
model . n_items ( )
} else {
( drop_bucket + 1 ) / 2
} ;
if let Ok ( Some ( path_str ) ) = drop_value . get ::< Option < String > > ( ) {
let desktop_path = & Path ::new ( & path_str ) ;
if let Some ( pathbase ) = desktop_path . file_name ( ) {
if let Some ( app_info ) = gio ::DesktopAppInfo ::new ( & pathbase . to_string_lossy ( ) ) {
// remove item if already exists
let mut i : u32 = 0 ;
let mut index_of_existing_app : Option < u32 > = None ;
while let Some ( item ) = model . item ( i ) {
if let Ok ( cur_app_info ) = item . downcast ::< DockObject > ( ) {
if let Ok ( Some ( cur_app_info ) ) = cur_app_info . property ( " appinfo " ) . expect ( " property appinfo missing from DockObject " ) . get ::< Option < DesktopAppInfo > > ( ) {
if cur_app_info . filename ( ) = = Some ( Path ::new ( & path_str ) . to_path_buf ( ) ) {
index_of_existing_app = Some ( i ) ;
}
}
}
i + = 1 ;
}
if let Some ( index_of_existing_app ) = index_of_existing_app {
// remove existing entry
model . remove ( index_of_existing_app ) ;
if let Some ( old_handle ) = drag_end . replace ( None ) {
glib ::signal_handler_disconnect ( & drag_source , old_handle ) ;
}
}
model . insert ( index , & DockObject ::new ( app_info ) ) ;
}
}
}
else if let Ok ( old_index ) = drop_value . get ::< u32 > ( ) {
if let Some ( item ) = model . item ( old_index ) {
if let Ok ( dock_object ) = item . downcast ::< DockObject > ( ) {
model . remove ( old_index ) ;
model . insert ( index , & dock_object ) ;
if let Some ( old_handle ) = drag_end . replace ( None ) {
glib ::signal_handler_disconnect ( & drag_source , old_handle ) ;
}
}
}
}
else {
// dbg!("rejecting drop");
_self . reject ( ) ;
}
glib ::MainContext ::default ( ) . spawn_local ( async move {
let _ = TX . get ( ) . unwrap ( ) . send ( Event ::RefreshFromCache ) . await ;
} ) ;
true
} ) ,
) ;
imp . drop_controller
. set ( drop_controller )
. expect ( " Could not set dock dnd drop controller " ) ;
}
fn setup_drag ( & self ) {
let imp = imp ::DockList ::from_instance ( self ) ;
let type_ = imp . type_ . get ( ) . unwrap ( ) ;
let actions = match type_ {
& DockListType ::Saved = > gdk4 ::DragAction ::MOVE ,
& DockListType ::Active = > gdk4 ::DragAction ::COPY ,
} ;
let drag_source = DragSource ::builder ( )
. name ( " dock drag source " )
. actions ( actions )
. build ( ) ;
let model = self . model ( ) ;
let list_view = imp . list_view . get ( ) . unwrap ( ) ;
let drag_end = & imp . drag_end_signal ;
let drag_cancel = & imp . drag_cancel_signal ;
let type_ = type_ . clone ( ) ;
list_view . add_controller ( & drag_source ) ;
drag_source . connect_prepare ( glib ::clone! ( @ weak model , @ weak list_view , @ weak drag_end , @ weak drag_cancel = > @ default - return None , move | self_ , x , _y | {
let max_x = list_view . allocated_width ( ) ;
// dbg!(max_x);
// dbg!(max_y);
let n_buckets = model . n_items ( ) ;
let index = ( x * n_buckets as f64 / ( max_x as f64 + 0.1 ) ) as u32 ;
if let Some ( item ) = model . item ( index ) {
if type_ = = DockListType ::Saved {
if let Some ( old_handle ) = drag_end . replace ( Some ( self_ . connect_drag_end (
glib ::clone! ( @ weak model = > move | _self , _drag , _delete_data | {
dbg! ( _delete_data ) ;
if _delete_data {
model . remove ( index ) ;
glib ::MainContext ::default ( ) . spawn_local ( async move {
if let Some ( tx ) = TX . get ( ) {
let _ = tx . send ( Event ::RefreshFromCache ) . await ;
}
} ) ;
} ;
} ) ,
) ) ) {
glib ::signal_handler_disconnect ( self_ , old_handle ) ;
}
if let Some ( old_handle ) = drag_cancel . replace ( Some ( self_ . connect_drag_cancel (
glib ::clone! ( @ weak model = > @ default - return false , move | _self , _drag , cancel_reason | {
if cancel_reason ! = gdk4 ::DragCancelReason ::UserCancelled {
model . remove ( index ) ;
glib ::MainContext ::default ( ) . spawn_local ( async move {
if let Some ( tx ) = TX . get ( ) {
let _ = tx . send ( Event ::RefreshFromCache ) . await ;
}
} ) ;
true
} else {
false
}
} ) ,
) ) ) {
glib ::signal_handler_disconnect ( self_ , old_handle ) ;
}
}
if let Ok ( dock_object ) = item . downcast ::< DockObject > ( ) {
if let Ok ( Some ( app_info ) ) = dock_object . property ( " appinfo " ) . expect ( " property appinfo missing from DockObject " ) . get ::< Option < DesktopAppInfo > > ( ) {
let icon = app_info
. icon ( )
. unwrap_or ( Icon ::for_string ( " image-missing " ) . expect ( " Failed to set default icon " ) ) ;
if let Some ( default_display ) = & Display ::default ( ) {
if let Some ( icon_theme ) = IconTheme ::for_display ( default_display ) {
if let Some ( paintable_icon ) = icon_theme . lookup_by_gicon (
& icon ,
64 ,
1 ,
gtk4 ::TextDirection ::None ,
gtk4 ::IconLookupFlags ::empty ( ) ,
) {
self_ . set_icon ( Some ( & paintable_icon ) , 32 , 32 ) ;
}
}
}
// saved app list provides index
return match type_ {
DockListType ::Saved = > Some ( ContentProvider ::for_value ( & index . to_value ( ) ) ) ,
DockListType ::Active = > app_info . filename ( ) . map ( | file | ContentProvider ::for_value ( & file . to_string_lossy ( ) . to_value ( ) ) )
}
}
}
}
None
} ) ) ;
imp . drag_source
. set ( drag_source )
. expect ( " Could not set saved drag source " ) ;
}
fn setup_factory ( & self ) {
let imp = imp ::DockList ::from_instance ( self ) ;
2022-01-03 21:33:26 -05:00
let popover_menu_index = & imp . popover_menu_index ;
let factory = SignalListItemFactory ::new ( ) ;
2022-01-02 01:46:44 -05:00
let model = imp . model . get ( ) . expect ( " Failed to get saved app model. " ) ;
2022-01-03 21:33:26 -05:00
factory . connect_setup (
glib ::clone! ( @ weak popover_menu_index , @ weak model = > move | _ , list_item | {
let dock_item = DockItem ::new ( ) ;
dock_item
. connect_local ( " popover-closed " , false , move | _ | {
if let Some ( old_index ) = popover_menu_index . replace ( None ) {
if let Some ( item ) = model . item ( old_index ) {
if let Ok ( dock_object ) = item . downcast ::< DockObject > ( ) {
dock_object . set_popover ( false ) ;
model . items_changed ( old_index , 0 , 0 ) ;
}
}
}
None
} )
. unwrap ( ) ;
list_item . set_child ( Some ( & dock_item ) ) ;
} ) ,
) ;
factory . connect_bind ( move | _ , list_item | {
2022-01-02 01:46:44 -05:00
let application_object = list_item
. item ( )
. expect ( " The item has to exist. " )
. downcast ::< DockObject > ( )
. expect ( " The item has to be a `DockObject` " ) ;
let dock_item = list_item
. child ( )
. expect ( " The list item child needs to exist. " )
. downcast ::< DockItem > ( )
. expect ( " The list item type needs to be `DockItem` " ) ;
dock_item . set_app_info ( & application_object ) ;
2022-01-03 21:33:26 -05:00
} ) ;
2022-01-02 01:46:44 -05:00
// Set the factory of the list view
imp . list_view . get ( ) . unwrap ( ) . set_factory ( Some ( & factory ) ) ;
}
}