move prototypes out of libcosmic

This commit is contained in:
Ashley Wulber 2022-01-24 12:03:22 -05:00
parent 36d317a3e3
commit 08e70faffd
41 changed files with 1 additions and 4312 deletions

View file

@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
cascade = "1.0.0"
derivative = "2.2.0"
gtk4 = { version = "0.4.4", features = ["v4_4"] }
gdk4 = "0.4.4"
gdk4-wayland = { version = "0.4.2", features = [ "wayland_crate" ], optional = true }
gdk4-x11 = { version = "0.4.2", features = [ "xlib" ] }
@ -14,30 +14,7 @@ gobject-sys = "0.15.1"
wayland-client = { version = "0.29.4", optional = true }
wayland-protocols = { version = "0.29.4", features = [ "client", "unstable_protocols" ], optional = true }
x11 = { version = "2.19.1", features = ["xlib"] }
# examples
gtk4 = { version = "0.4.4", features = ["v4_4"] }
gtk4-sys = "0.4.2"
glib-sys = "0.15.1"
relm4-macros = { git = "https://github.com/AaronErhardt/Relm4" }
pop-launcher-service = { git = "https://github.com/wash2/launcher.git" }
pop-launcher = { git = "https://github.com/wash2/launcher.git" }
serde = "1.0.134"
serde_json = "1.0.75"
tokio = { version = "1.15.0", features = ["sync"] }
futures = "0.3.19"
futures-util = "0.3.19"
once_cell = "1.9.0"
xdg = "2.4.0"
# zbus
zbus = "2.0.1"
zvariant = "3.1.0"
zvariant_derive = "3.1.0"
libloading = "0.7.3"
[profile.release]
incremental = true
debug = 1
[features]
layer-shell = ["gdk4-wayland", "wayland-client", "wayland-protocols"]

View file

@ -1,2 +0,0 @@
# Applications Library

View file

@ -1,26 +0,0 @@
use gtk4::subclass::prelude::*;
use gtk4::{gio, glib, GridView};
use once_cell::sync::OnceCell;
#[derive(Default)]
pub struct AppGrid {
pub app_grid_view: OnceCell<GridView>,
pub app_model: OnceCell<gio::ListStore>,
pub app_sort_model: OnceCell<gtk4::SortListModel>,
pub search_filter_model: OnceCell<gtk4::FilterListModel>,
pub group_filter_model: OnceCell<gtk4::FilterListModel>,
}
#[glib::object_subclass]
impl ObjectSubclass for AppGrid {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "AppGrid";
type Type = super::AppGrid;
type ParentType = gtk4::Box;
}
impl ObjectImpl for AppGrid {}
impl WidgetImpl for AppGrid {}
impl BoxImpl for AppGrid {}

View file

@ -1,196 +0,0 @@
use cascade::cascade;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::{gio, glib, GridView, PolicyType, ScrolledWindow, SignalListItemFactory};
use crate::grid_item::GridItem;
mod imp;
glib::wrapper! {
pub struct AppGrid(ObjectSubclass<imp::AppGrid>)
@extends gtk4::Widget, gtk4::Box,
@implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Orientable;
}
impl Default for AppGrid {
fn default() -> Self {
Self::new()
}
}
impl AppGrid {
pub fn new() -> Self {
let self_: Self = glib::Object::new(&[]).expect("Failed to create AppGrid");
let imp = imp::AppGrid::from_instance(&self_);
let library_window = cascade! {
ScrolledWindow::new();
..set_hscrollbar_policy(PolicyType::Never);
..set_min_content_height(520);
..set_hexpand(true);
..set_margin_top(12);
};
self_.append(&library_window);
let library_grid = cascade! {
GridView::default();
..set_min_columns(7);
..set_max_columns(7);
..set_single_click_activate(true);
};
library_window.set_child(Some(&library_grid));
imp.app_grid_view.set(library_grid).unwrap();
// Setup
self_.setup_model();
self_.setup_callbacks();
self_.setup_factory();
self_
}
fn setup_model(&self) {
// Create new model
let app_model = gio::ListStore::new(gio::DesktopAppInfo::static_type());
// Get state and set model
let imp = imp::AppGrid::from_instance(self);
// A sorter used to sort AppInfo in the model by their name
xdg::BaseDirectories::new()
.expect("could not access XDG Base directory")
.get_data_dirs()
.iter_mut()
.for_each(|xdg_data_path| {
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() {
app_model.append(&app_info)
} else {
// println!("Ignoring {}", path);
}
} else {
// println!("error loading {}", path);
}
}
}
}
})
}
});
let sorter = gtk4::CustomSorter::new(move |obj1, obj2| {
let app_info1 = obj1.downcast_ref::<gio::DesktopAppInfo>().unwrap();
let app_info2 = obj2.downcast_ref::<gio::DesktopAppInfo>().unwrap();
app_info1
.name()
.to_lowercase()
.cmp(&app_info2.name().to_lowercase())
.into()
});
let filter = gtk4::CustomFilter::new(|_obj| true);
let search_filter_model =
gtk4::FilterListModel::new(Some(&app_model), Some(filter).as_ref());
let filter = gtk4::CustomFilter::new(|_obj| true);
let group_filter_model =
gtk4::FilterListModel::new(Some(&search_filter_model), Some(filter).as_ref());
let sorted_model = gtk4::SortListModel::new(Some(&group_filter_model), Some(&sorter));
let selection_model = gtk4::SingleSelection::builder()
.model(&sorted_model)
.autoselect(false)
.can_unselect(true)
.selected(gtk4::INVALID_LIST_POSITION)
.build();
// Wrap model with selection and pass it to the list view
imp.app_model
.set(app_model.clone())
.expect("Could not set model");
imp.app_sort_model.set(sorted_model).unwrap();
imp.search_filter_model.set(search_filter_model).unwrap();
imp.group_filter_model.set(group_filter_model).unwrap();
imp.app_grid_view
.get()
.unwrap()
.set_model(Some(&selection_model));
selection_model.unselect_all();
}
fn setup_callbacks(&self) {
let imp = imp::AppGrid::from_instance(self);
let app_grid_view = &imp.app_grid_view.get().unwrap();
app_grid_view.connect_activate(move |list_view, i| {
// on activation change the group filter model to use the app names, and category
// println!("selected app {}", i);
// Launch the application when an item of the list is activated
let model = list_view.model().unwrap();
if let Some(item) = model.item(i) {
let app_info = item.downcast::<gio::DesktopAppInfo>().unwrap();
let context = list_view.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)
.build()
.show();
}
}
});
}
fn setup_factory(&self) {
let app_factory = SignalListItemFactory::new();
app_factory.connect_setup(move |_factory, item| {
let row = GridItem::new();
item.set_child(Some(&row));
});
let imp = imp::AppGrid::from_instance(self);
// the bind stage is used for "binding" the data to the created widgets on the "setup" stage
let app_grid_view = &imp.app_grid_view.get().unwrap();
app_factory.connect_bind(
glib::clone!(@weak app_grid_view => move |_factory, grid_item| {
let app_info = grid_item
.item()
.unwrap()
.downcast::<gio::DesktopAppInfo>()
.unwrap();
let child = grid_item.child().unwrap().downcast::<GridItem>().unwrap();
child.set_app_info(&app_info);
}),
);
// Set the factory of the list view
app_grid_view.set_factory(Some(&app_factory));
}
pub fn set_app_sorter(&self, sorter: &gtk4::CustomSorter) {
let imp = imp::AppGrid::from_instance(&self);
let sort_model = imp.app_sort_model.get().unwrap();
sort_model.set_sorter(Some(sorter));
}
pub fn set_search_filter(&self, filter: &gtk4::CustomFilter) {
let imp = imp::AppGrid::from_instance(&self);
let filter_model = imp.search_filter_model.get().unwrap();
filter_model.set_filter(Some(filter));
}
pub fn set_group_filter(&self, filter: &gtk4::CustomFilter) {
let imp = imp::AppGrid::from_instance(&self);
let filter_model = imp.group_filter_model.get().unwrap();
filter_model.set_filter(Some(filter));
}
}

View file

@ -1,62 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use gdk4::glib::ParamSpecBoxed;
use glib::{ParamFlags, ParamSpec, Value};
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use once_cell::sync::Lazy;
use super::BoxedAppGroupType;
// Object holding the state
#[derive(Default)]
pub struct AppGroup {
pub inner: Rc<RefCell<BoxedAppGroupType>>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for AppGroup {
const NAME: &'static str = "AppGroup";
type Type = super::AppGroup;
type ParentType = glib::Object;
}
// Trait shared by all GObjects
impl ObjectImpl for AppGroup {
fn properties() -> &'static [ParamSpec] {
static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
vec![ParamSpecBoxed::new(
// Name
"inner",
// Nickname
"inner",
// Short description
"inner",
BoxedAppGroupType::static_type(),
// The property can be read and written to
ParamFlags::READWRITE,
)]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
match pspec.name() {
"inner" => {
let inner = value.get().expect("The value needs to be of type `u32`.");
self.inner.replace(inner);
}
_ => unimplemented!(),
}
}
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value {
match pspec.name() {
"inner" => self.inner.borrow().to_value(),
_ => unimplemented!(),
}
}
}

View file

@ -1,88 +0,0 @@
use glib::Object;
use gtk4::glib;
use gtk4::subclass::prelude::*;
use serde::{Deserialize, Serialize};
mod imp;
glib::wrapper! {
pub struct AppGroup(ObjectSubclass<imp::AppGroup>);
}
impl AppGroup {
pub fn new(data: BoxedAppGroupType) -> Self {
let self_: Self =
Object::new(&[("inner", &data)]).expect("Failed to create `ApplicationObject`.");
self_
}
pub fn popup(&self) {
let imp = imp::AppGroup::from_instance(self);
let inner = imp.inner.borrow().clone();
match inner {
BoxedAppGroupType::Group(d) => {
// d.popup = true;
imp.inner.replace(BoxedAppGroupType::Group(d));
}
BoxedAppGroupType::NewGroup(_) => {
imp.inner.replace(BoxedAppGroupType::NewGroup(true));
}
};
}
pub fn popdown(&self) {
let imp = imp::AppGroup::from_instance(self);
let inner = imp.inner.borrow().clone();
match inner {
BoxedAppGroupType::Group(d) => {
// d.popup = false;
imp.inner.replace(BoxedAppGroupType::Group(d));
}
BoxedAppGroupType::NewGroup(_) => {
imp.inner.replace(BoxedAppGroupType::NewGroup(false));
}
};
}
pub fn is_popup_active(&self) -> bool {
let imp = imp::AppGroup::from_instance(self);
match imp.inner.borrow().clone() {
BoxedAppGroupType::Group(_d) => false,
BoxedAppGroupType::NewGroup(is_active) => is_active,
}
}
pub fn group_data(&self) -> Option<AppGroupData> {
let imp = imp::AppGroup::from_instance(self);
let inner = imp.inner.borrow().clone();
match inner {
BoxedAppGroupType::Group(d) => Some(d),
_ => None,
}
}
}
#[derive(Serialize, Deserialize, Clone, glib::Boxed)]
#[boxed_type(name = "BoxedAppGroupType")]
pub enum BoxedAppGroupType {
Group(AppGroupData),
NewGroup(bool),
}
impl Default for BoxedAppGroupType {
fn default() -> Self {
Self::NewGroup(false)
}
}
// Object holding the state
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct AppGroupData {
pub id: u32,
pub name: String,
pub icon: String,
pub mutable: bool,
pub app_names: Vec<String>,
pub category: String,
// pub popup: bool,
}

View file

@ -1,55 +0,0 @@
use glib::subclass::Signal;
use gtk4::subclass::prelude::*;
use once_cell::sync::Lazy;
use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Rc;
use gtk4::{glib, prelude::*, Popover};
#[derive(Debug, Default)]
pub struct GridItem {
pub(super) name: Rc<RefCell<gtk4::Label>>,
pub(super) image: Rc<RefCell<gtk4::Image>>,
pub(super) index: Cell<u32>,
pub(super) popover: Rc<RefCell<Option<Popover>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for GridItem {
const NAME: &'static str = "GridItem";
type Type = super::GridItem;
type ParentType = gtk4::Box;
}
impl ObjectImpl for GridItem {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder(
// Signal name
"new-group",
// Types of the values which will be sent to the signal handler
&[String::static_type().into()],
// Type of the value the signal handler sends back
<()>::static_type().into(),
)
.build(),
Signal::builder(
// Signal name
"popover-closed",
// Types of the values which will be sent to the signal handler
&[],
// Type of the value the signal handler sends back
<()>::static_type().into(),
)
.build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for GridItem {}
impl BoxImpl for GridItem {}

View file

@ -1,199 +0,0 @@
use cascade::cascade;
use gdk4::ContentProvider;
use gdk4::Display;
use gio::File;
use gio::Icon;
use gtk4::pango::EllipsizeMode;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::traits::WidgetExt;
use gtk4::Align;
use gtk4::Button;
use gtk4::DragSource;
use gtk4::IconTheme;
use gtk4::Image;
use gtk4::Label;
use gtk4::Orientation;
use gtk4::{gio, glib};
use crate::app_group::AppGroup;
use crate::app_group::BoxedAppGroupType;
mod imp;
glib::wrapper! {
pub struct GridItem(ObjectSubclass<imp::GridItem>)
@extends gtk4::Widget, gtk4::Box,
@implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Orientable;
}
impl Default for GridItem {
fn default() -> Self {
Self::new()
}
}
impl GridItem {
pub fn new() -> Self {
let self_ = glib::Object::new(&[]).expect("Failed to create GridItem");
let imp = imp::GridItem::from_instance(&self_);
cascade! {
&self_;
..set_orientation(Orientation::Vertical);
..set_halign(Align::Center);
..set_hexpand(true);
..set_margin_top(4);
..set_margin_bottom(4);
..set_margin_end(4);
..set_margin_start(4);
};
let image = cascade! {
Image::new();
..set_margin_top(4);
..set_margin_bottom(4);
..set_pixel_size(64);
};
self_.append(&image);
let name = cascade! {
Label::new(None);
..set_halign(Align::Center);
..set_hexpand(true);
..set_ellipsize(EllipsizeMode::End);
..add_css_class("title-5");
};
self_.append(&name);
imp.name.replace(name);
imp.image.replace(image);
self_
}
pub fn set_app_info(&self, app_info: &gio::DesktopAppInfo) {
let self_ = imp::GridItem::from_instance(self);
self_.name.borrow().set_text(&app_info.name());
let drag_controller = DragSource::builder()
.name("application library drag source")
.actions(gdk4::DragAction::COPY)
// .content()
.build();
self.add_controller(&drag_controller);
if let Some(file) = app_info.filename() {
let file = File::for_path(file);
let provider = ContentProvider::for_value(&file.to_value());
drag_controller.set_content(Some(&provider));
}
let icon = app_info
.icon()
.unwrap_or(Icon::for_string("image-missing").expect("Failed to set default icon"));
self_.image.borrow().set_from_gicon(&icon);
drag_controller.connect_drag_begin(glib::clone!(@weak icon, => move |_self, drag| {
drag.set_selected_action(gdk4::DragAction::MOVE);
// set drag source icon if possible...
// gio Icon is not easily converted to a Paintable, but this seems to be the correct method
if let Some(default_display) = &Display::default() {
let icon_theme = IconTheme::for_display(default_display);
let paintable_icon = icon_theme.lookup_by_gicon(
&icon,
64,
1,
gtk4::TextDirection::None,
gtk4::IconLookupFlags::empty(),
);
_self.set_icon(Some(&paintable_icon), 32, 32);
}
}));
}
pub fn set_group_info(&self, app_group: AppGroup) {
// if data type set name and icon to values in data
let imp = imp::GridItem::from_instance(self);
match app_group.property::<BoxedAppGroupType>("inner") {
BoxedAppGroupType::Group(data) => {
imp.name.borrow().set_text(&data.name);
imp.image.borrow().set_from_icon_name(Some(&data.icon));
}
BoxedAppGroupType::NewGroup(popover_active) => {
// else must be add group
imp.name.borrow().set_text("New Group");
imp.image.borrow().set_from_icon_name(Some("folder-new"));
let popover_menu = gtk4::Box::builder()
.spacing(12)
.hexpand(true)
.orientation(gtk4::Orientation::Vertical)
.margin_top(12)
.margin_bottom(12)
.margin_end(12)
.margin_start(12)
.build();
// build menu
let dialog_entry = gtk4::Entry::new();
let label = cascade! {
Label::new(Some("Name"));
..set_justify(gtk4::Justification::Left);
..set_xalign(0.0);
};
popover_menu.append(&label);
popover_menu.append(&dialog_entry);
let btn_container = cascade! {
gtk4::Box::new(Orientation::Horizontal, 8);
};
let ok_btn = cascade! {
Button::with_label("Ok");
};
let cancel_btn = cascade! {
Button::with_label("Cancel");
};
btn_container.append(&ok_btn);
btn_container.append(&cancel_btn);
popover_menu.append(&btn_container);
let popover = cascade! {
gtk4::Popover::new();
..set_autohide(true);
..set_child(Some(&popover_menu));
};
self.append(&popover);
popover.connect_closed(
glib::clone!(@weak self as self_, @weak dialog_entry => move |_| {
dialog_entry.set_text("");
self_.emit_by_name::<()>("popover-closed", &[]);
}),
);
ok_btn.connect_clicked(
glib::clone!(@weak self as self_, @weak dialog_entry, @weak popover => move |_| {
let new_name = dialog_entry.text().to_string();
popover.popdown();
glib::idle_add_local_once(glib::clone!(@weak self_ => move || {
self_.emit_by_name::<()>("new-group", &[&new_name]);
}));
}),
);
cancel_btn.connect_clicked(glib::clone!(@weak popover => move |_| {
popover.popdown();
}));
if popover_active {
popover.popup();
}
imp.popover.replace(Some(popover));
}
}
}
pub fn set_index(&self, index: u32) {
imp::GridItem::from_instance(self).index.set(index);
}
pub fn popup(&self) {
let imp = imp::GridItem::from_instance(self);
if let Some(popover) = imp.popover.borrow().as_ref() {
popover.popup();
}
}
}

View file

@ -1,41 +0,0 @@
use glib::subclass::Signal;
use gtk4::subclass::prelude::*;
use gtk4::{gio, glib, GridView, ScrolledWindow};
use gtk4::{prelude::*, CustomFilter};
use once_cell::sync::{Lazy, OnceCell};
#[derive(Default)]
pub struct GroupGrid {
pub group_grid_view: OnceCell<GridView>,
pub group_scroll_window: OnceCell<ScrolledWindow>,
pub group_model: OnceCell<gio::ListStore>,
}
#[glib::object_subclass]
impl ObjectSubclass for GroupGrid {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "GroupGrid";
type Type = super::GroupGrid;
type ParentType = gtk4::Box;
}
impl ObjectImpl for GroupGrid {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder(
// Signal name
"group-changed",
// Types of the values which will be sent to the signal handler
&[CustomFilter::static_type().into()],
// Type of the value the signal handler sends back
<()>::static_type().into(),
)
.build()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for GroupGrid {}
impl BoxImpl for GroupGrid {}

View file

@ -1,287 +0,0 @@
use cascade::cascade;
use glib::Object;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::{gio, glib, GridView, PolicyType, ScrolledWindow, SignalListItemFactory};
use std::fs::File;
use crate::app_group::{AppGroup, AppGroupData, BoxedAppGroupType};
use crate::grid_item::GridItem;
use crate::utils::data_path;
use crate::utils::set_group_scroll_policy;
mod imp;
glib::wrapper! {
pub struct GroupGrid(ObjectSubclass<imp::GroupGrid>)
@extends gtk4::Widget, gtk4::Box,
@implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Orientable;
}
impl Default for GroupGrid {
fn default() -> Self {
Self::new()
}
}
impl GroupGrid {
pub fn new() -> Self {
let self_: Self = glib::Object::new(&[]).expect("Failed to create GroupGrid");
let imp = imp::GroupGrid::from_instance(&self_);
let group_window = cascade! {
ScrolledWindow::new();
..set_hscrollbar_policy(PolicyType::Never);
..set_vscrollbar_policy(PolicyType::Never);
..set_propagate_natural_height(true);
..set_min_content_height(150);
..set_max_content_height(300);
..set_hexpand(true);
};
self_.append(&group_window);
let group_grid_view = cascade! {
GridView::default();
..set_min_columns(8);
..set_max_columns(8);
};
group_window.set_child(Some(&group_grid_view));
imp.group_grid_view.set(group_grid_view).unwrap();
imp.group_scroll_window.set(group_window).unwrap();
// Setup
// Setup
self_.setup_model();
self_.restore_data();
self_.setup_callbacks();
self_.setup_factory();
self_
}
fn setup_model(&self) {
let imp = imp::GroupGrid::from_instance(&self);
let group_model = gio::ListStore::new(AppGroup::static_type());
imp.group_model
.set(group_model.clone())
.expect("Could not set group model");
vec![
AppGroup::new(BoxedAppGroupType::Group(AppGroupData {
id: 0,
name: "Library Home".to_string(),
icon: "user-home".to_string(),
mutable: false,
app_names: Vec::new(),
category: "".to_string(),
})),
AppGroup::new(BoxedAppGroupType::Group(AppGroupData {
id: 0,
name: "System".to_string(),
icon: "folder".to_string(),
mutable: false,
app_names: Vec::new(),
category: "System".to_string(),
})),
AppGroup::new(BoxedAppGroupType::Group(AppGroupData {
id: 0,
name: "Utilities".to_string(),
icon: "folder".to_string(),
mutable: false,
app_names: Vec::new(),
category: "Utility".to_string(),
})),
// Example of group with app name
// AppGroup::new(AppGroupData {
// id: 0,
// name: "Custom Web".to_string(),
// icon: "folder".to_string(),
// mutable: true,
// app_names: vec!["Firefox Web Browser".to_string()],
// category: "".to_string(),
// }),
AppGroup::new(BoxedAppGroupType::NewGroup(false)),
]
.iter()
.for_each(|group| {
group_model.append(group);
});
let group_selection = gtk4::SingleSelection::new(Some(&group_model));
imp.group_grid_view
.get()
.unwrap()
.set_model(Some(&group_selection));
}
fn group_model(&self) -> &gio::ListStore {
// Get state
let imp = imp::GroupGrid::from_instance(self);
imp.group_model.get().expect("Could not get model")
}
fn setup_callbacks(&self) {
let imp = imp::GroupGrid::from_instance(self);
let group_grid_view = &imp.group_grid_view.get().unwrap();
let scroll_window = &imp.group_scroll_window.get().unwrap();
// dynamically set scroll method
self.group_model().connect_items_changed(
glib::clone!(@weak scroll_window => move |scroll_list_model, _i, _rmv_cnt, _add_cnt| {
set_group_scroll_policy(&scroll_window, scroll_list_model.n_items());
}),
);
let self_clone = self.clone();
group_grid_view.connect_activate(move |group_grid_view, i| {
// on activation change the group filter model to use the app names, and category
println!("grid view activated. {}", i);
let group_model = group_grid_view
.model()
.unwrap()
.downcast::<gtk4::SingleSelection>()
.unwrap()
.model()
.downcast::<gio::ListStore>()
.expect("could not downcast app group view selection model to list store model");
// update the application filter
if let Some(data) = group_model
.item(i)
.unwrap()
.downcast::<AppGroup>()
.unwrap()
.group_data()
{
let category = data.category.to_lowercase();
let new_filter: gtk4::CustomFilter = gtk4::CustomFilter::new(move |obj| {
let app = obj
.downcast_ref::<gio::DesktopAppInfo>()
.expect("The Object needs to be of type AppInfo");
if data.app_names.len() > 0 {
return data.app_names.contains(&String::from(app.name().as_str()));
}
match app.categories() {
Some(categories) => {
categories.to_string().to_lowercase().contains(&category)
}
None => false,
}
});
self_clone.emit_by_name::<()>("group-changed", &[&new_filter]);
} else {
// don't change filter, instead show dialog for adding new group!
let item = group_model.item(i).unwrap().downcast::<AppGroup>().unwrap();
item.popup();
group_model.items_changed(i, 0, 0);
}
});
}
pub fn is_popup_active(&self) -> bool {
let model = self.group_model();
for i in 0..model.n_items() {
let item = model.item(i).unwrap().downcast::<AppGroup>().unwrap();
if item.is_popup_active() {
return true;
}
}
return false;
}
fn setup_factory(&self) {
let imp = imp::GroupGrid::from_instance(&self);
let group_factory = SignalListItemFactory::new();
group_factory.connect_setup(glib::clone!(@weak self as self_ => move |_factory, item| {
let obj = GridItem::new();
item.set_child(Some(&obj));
obj
.connect_local("new-group", false, glib::clone!(@weak self_ => @default-return None, move |args| {
let m = self_.group_model();
match args[1].get::<String>() {
Ok(name) => {
let new_group = AppGroup::new(BoxedAppGroupType::Group(AppGroupData {
id: 0,
name: name,
icon: "folder".to_string(),
mutable: false,
app_names: Vec::new(),
category: "".to_string(),
})).upcast::<Object>();
m.insert(m.n_items() - 1, &new_group);
self_.store_data();
}
_ => unimplemented!(),
};
None
}));
obj
.connect_local("popover-closed", false, glib::clone!(@weak self_ => @default-return None, move |_| {
let m = self_.group_model();
let group = m.item(m.n_items() - 1).unwrap().downcast::<AppGroup>().unwrap();
glib::idle_add_local_once(move || {
group.popdown();
});
None
}));
}));
// the bind stage is used for "binding" the data to the created widgets on the "setup" stage
group_factory.connect_bind(move |_factory, grid_item| {
let group_info = grid_item.item().unwrap().downcast::<AppGroup>().unwrap();
let child = grid_item.child().unwrap().downcast::<GridItem>().unwrap();
child.set_group_info(group_info);
});
// Set the factory of the list view
imp.group_grid_view
.get()
.unwrap()
.set_factory(Some(&group_factory));
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<AppGroupData> =
serde_json::from_reader(file).expect("Could not get backup data from json file.");
let app_group_objects: Vec<Object> = backup_data
.into_iter()
.map(|data| AppGroup::new(BoxedAppGroupType::Group(data)).upcast::<Object>())
.collect();
let scroll_window = &imp::GroupGrid::from_instance(self)
.group_scroll_window
.get()
.unwrap();
// Insert restored objects into model
self.group_model().splice(3, 0, &app_group_objects);
set_group_scroll_policy(&scroll_window, self.group_model().n_items());
} else {
println!("Backup file does not exist yet {:?}", data_path());
}
}
pub fn store_data(&self) {
let mut backup_data = Vec::new();
let mut position = 3;
while let Some(item) = self.group_model().item(position) {
if position == self.group_model().n_items() - 1 {
break;
}
// Get `AppGroup` from `glib::Object`
let group_data = item
.downcast_ref::<AppGroup>()
.expect("The object needs to be of type `AppGroupData`.")
.group_data();
// Add data to vector and increase position
backup_data.push(group_data);
position += 1;
}
// 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");
}
}

View file

@ -1,49 +0,0 @@
use gtk4::gdk::Display;
use gtk4::prelude::*;
use gtk4::CssProvider;
use gtk4::StyleContext;
use window::AppLibraryWindow;
mod app_grid;
mod app_group;
mod grid_item;
mod group_grid;
mod utils;
mod window;
mod window_inner;
fn main() {
let app = gtk4::Application::new(Some("com.cosmic.app_library"), Default::default());
app.connect_startup(|_app| {
load_css();
});
app.connect_activate(|app| {
build_ui(app);
});
app.run();
}
fn load_css() {
// Load the css file and add it to the provider
let provider = CssProvider::new();
provider.load_from_data(include_bytes!("style.css"));
// Add the provider to the default screen
StyleContext::add_provider_for_display(
&Display::default().expect("Error initializing GTK CSS provider."),
&provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn build_ui(app: &gtk4::Application) {
// Create a new custom window and show it
let display = Display::default().unwrap();
window::create(app, display.monitors().item(0).unwrap().downcast().unwrap());
// let window = AppLibraryWindow::new(app);
// window.show();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View file

@ -1,23 +0,0 @@
child:selected {
transition: 100ms;
background: #AAAAAA;
}
gridview child {
transition: 100ms;
border-radius: 5px;
}
gridview {
background: #333333;
}
box.app_library_container {
background: #333333;
padding: 12px;
border-radius: 12px;
}
window.root_window {
background: rgba(50, 50, 50, 0.0);
}

View file

@ -1,20 +0,0 @@
use std::path::PathBuf;
use gtk4::glib;
use gtk4::ScrolledWindow;
pub fn data_path() -> PathBuf {
let mut path = glib::user_data_dir();
path.push("com.cosmic.app_library");
std::fs::create_dir_all(&path).expect("Could not create directory.");
path.push("data.json");
path
}
pub fn set_group_scroll_policy(scroll_window: &ScrolledWindow, group_cnt: u32) {
if scroll_window.policy().1 == gtk4::PolicyType::Never && group_cnt > 16 {
scroll_window.set_policy(gtk4::PolicyType::Never, gtk4::PolicyType::Automatic);
} else if scroll_window.policy().1 == gtk4::PolicyType::Automatic && group_cnt <= 16 {
scroll_window.set_policy(gtk4::PolicyType::Never, gtk4::PolicyType::Never);
}
}

View file

@ -1,32 +0,0 @@
use crate::window_inner::AppLibraryWindowInner;
use gtk4::glib;
use gtk4::subclass::prelude::*;
use once_cell::sync::OnceCell;
// Object holding the state
#[derive(Default)]
pub struct AppLibraryWindow {
pub(super) inner: OnceCell<AppLibraryWindowInner>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for AppLibraryWindow {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "AppLibraryWindow";
type Type = super::AppLibraryWindow;
type ParentType = gtk4::ApplicationWindow;
}
// Trait shared by all GObjects
impl ObjectImpl for AppLibraryWindow {}
// Trait shared by all widgets
impl WidgetImpl for AppLibraryWindow {}
// Trait shared by all windows
impl WindowImpl for AppLibraryWindow {}
// Trait shared by all application
impl ApplicationWindowImpl for AppLibraryWindow {}

View file

@ -1,171 +0,0 @@
use crate::window_inner::AppLibraryWindowInner;
use cascade::cascade;
use gdk4::subclass::prelude::ObjectSubclassExt;
use gdk4_x11::X11Display;
use glib::Object;
use gtk4::prelude::*;
use gtk4::Application;
use gtk4::{gdk, gio, glib};
use libcosmic::x;
pub fn create(app: &Application, monitor: gdk::Monitor) {
//quit shortcut
app.set_accels_for_action("app.quit", &["<primary>W", "Escape"]);
setup_shortcuts(app);
#[cfg(feature = "layer-shell")]
if let Some(wayland_monitor) = monitor.downcast_ref() {
wayland_create(&app, wayland_monitor);
return;
}
cascade! {
AppLibraryWindow::new(&app);
..show();
};
}
fn setup_shortcuts(app: &Application) {
let action_quit = gio::SimpleAction::new("quit", None);
action_quit.connect_activate(glib::clone!(@weak app => move |_, _| {
app.quit();
}));
app.add_action(&action_quit);
}
#[cfg(feature = "layer-shell")]
fn wayland_create(app: &Application, monitor: &gdk4_wayland::WaylandMonitor) {
use libcosmic::wayland::{Anchor, KeyboardInteractivity, Layer, LayerShellWindow};
let window = cascade! {
LayerShellWindow::new(Some(monitor), Layer::Top, "");
..set_width_request(800);
..set_height_request(600);
// ..set_title(Some("Cosmic App Library"));
// ..set_decorated(false);
..set_keyboard_interactivity(KeyboardInteractivity::OnDemand);
..add_css_class("root_window");
..set_anchor(Anchor::empty());
..show();
};
let app_library = AppLibraryWindowInner::new();
window.set_child(Some(&app_library));
dbg!(&window);
window.connect_is_active_notify(glib::clone!(@weak app => move |w| {
if !w.is_active() {
app.quit();
}
}));
window.show();
// setup_shortcuts(window.clone().upcast::<gtk4::ApplicationWindow>());
// XXX
unsafe { window.set_data("cosmic-app-hold", app.hold()) };
}
mod imp;
glib::wrapper! {
pub struct AppLibraryWindow(ObjectSubclass<imp::AppLibraryWindow>)
@extends gtk4::ApplicationWindow, gtk4::Window, gtk4::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk4::Accessible, gtk4::Buildable,
gtk4::ConstraintTarget, gtk4::Native, gtk4::Root, gtk4::ShortcutManager;
}
impl AppLibraryWindow {
pub fn new(app: &Application) -> Self {
let self_: Self =
Object::new(&[("application", app)]).expect("Failed to create `AppLibraryWindow`.");
let imp = imp::AppLibraryWindow::from_instance(&self_);
cascade! {
&self_;
..set_width_request(1200);
..set_title(Some("Cosmic App Library"));
..set_decorated(false);
..add_css_class("root_window");
};
let app_library = AppLibraryWindowInner::new();
self_.set_child(Some(&app_library));
imp.inner.set(app_library).unwrap();
Self::setup_callbacks(&self_);
self_
}
fn setup_callbacks(&self) {
// Get state
let window = self.clone().upcast::<gtk4::Window>();
window.connect_realize(move |window| {
println!("gtk window setup");
if let Some((display, surface)) = x::get_window_x11(window) {
// ignore all x11 errors...
let xdisplay = display
.clone()
.downcast::<X11Display>()
.expect("Failed to downgrade X11 Display.");
xdisplay.error_trap_push();
unsafe {
x::change_property(
&display,
&surface,
"_NET_WM_WINDOW_TYPE",
x::PropMode::Replace,
&[x::Atom::new(&display, "_NET_WM_WINDOW_TYPE_DIALOG").unwrap()],
);
}
let resize = glib::clone!(@weak window => move || {
let height = window.height();
let width = window.width();
if let Some((display, _surface)) = x::get_window_x11(&window) {
let geom = display
.primary_monitor().geometry();
let monitor_x = geom.x();
let monitor_y = geom.y();
let monitor_width = geom.width();
let monitor_height = geom.height();
// dbg!(monitor_width);
// dbg!(monitor_height);
// dbg!(width);
// dbg!(height);
unsafe { x::set_position(&display, &surface,
monitor_x + monitor_width / 2 - width / 2,
monitor_y + monitor_height / 2 - height / 2)};
}
});
let s = window.surface();
let resize_height = resize.clone();
s.connect_height_notify(move |_s| {
glib::source::idle_add_local_once(resize_height.clone());
});
let resize_width = resize.clone();
s.connect_width_notify(move |_s| {
glib::source::idle_add_local_once(resize_width.clone());
});
s.connect_scale_factor_notify(move |_s| {
glib::source::idle_add_local_once(resize.clone());
});
} else {
println!("failed to get X11 window");
}
});
let imp = imp::AppLibraryWindow::from_instance(&self);
let inner = imp.inner.get().unwrap();
window.connect_is_active_notify(glib::clone!(@weak inner => move |win| {
let app = win
.application()
.expect("could not get application from window");
let active_window = app
.active_window()
.expect("no active window available, closing app library.");
if win == &active_window && !win.is_active() && !inner.is_popup_active() {
win.close();
}
}));
}
}

View file

@ -1,28 +0,0 @@
use gtk4::glib;
use gtk4::subclass::prelude::*;
use gtk4::SearchEntry;
use once_cell::sync::OnceCell;
use crate::app_grid::AppGrid;
use crate::group_grid::GroupGrid;
#[derive(Default)]
pub struct AppLibraryWindowInner {
pub entry: OnceCell<SearchEntry>,
pub app_grid: OnceCell<AppGrid>,
pub group_grid: OnceCell<GroupGrid>,
}
#[glib::object_subclass]
impl ObjectSubclass for AppLibraryWindowInner {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "AppLibraryWindowInner";
type Type = super::AppLibraryWindowInner;
type ParentType = gtk4::Box;
}
impl ObjectImpl for AppLibraryWindowInner {}
impl WidgetImpl for AppLibraryWindowInner {}
impl BoxImpl for AppLibraryWindowInner {}

View file

@ -1,136 +0,0 @@
use cascade::cascade;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::{gio, glib, Align, CustomFilter, Orientation, SearchEntry, Separator};
use crate::app_grid::AppGrid;
use crate::group_grid::GroupGrid;
mod imp;
glib::wrapper! {
pub struct AppLibraryWindowInner(ObjectSubclass<imp::AppLibraryWindowInner>)
@extends gtk4::Widget, gtk4::Box,
@implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Orientable;
}
impl Default for AppLibraryWindowInner {
fn default() -> Self {
Self::new()
}
}
impl AppLibraryWindowInner {
pub fn new() -> Self {
let self_: Self = glib::Object::new(&[]).expect("Failed to create AppLibraryWindowInner");
let imp = imp::AppLibraryWindowInner::from_instance(&self_);
cascade! {
&self_;
..set_orientation(Orientation::Vertical);
..add_css_class("app_library_container");
};
let entry = cascade! {
SearchEntry::new();
..set_width_request(300);
..set_halign(Align::Center);
..set_margin_top(12);
..set_margin_bottom(12);
..set_placeholder_text(Some(" Type to search"));
};
self_.append(&entry);
let app_grid = AppGrid::new();
self_.append(&app_grid);
let separator = cascade! {
Separator::new(Orientation::Horizontal);
..set_hexpand(true);
..set_margin_bottom(12);
..set_margin_top(12);
};
self_.append(&separator);
let group_grid = GroupGrid::new();
self_.append(&group_grid);
imp.entry.set(entry).unwrap();
imp.app_grid.set(app_grid).unwrap();
imp.group_grid.set(group_grid).unwrap();
Self::setup_callbacks(&self_);
self_
}
pub fn group_grid(&self) -> Option<&GroupGrid> {
let imp = imp::AppLibraryWindowInner::from_instance(self);
imp.group_grid.get()
}
pub fn is_popup_active(&self) -> bool {
if let Some(group_grid) = self.group_grid() {
group_grid.is_popup_active()
} else {
false
}
}
fn setup_callbacks(&self) {
// Get state
let imp = imp::AppLibraryWindowInner::from_instance(self);
let app_grid = &imp.app_grid.get().unwrap();
let group_grid = &imp.group_grid.get().unwrap();
let entry = &imp.entry.get().unwrap();
group_grid.connect_local(
"group-changed",
false,
glib::clone!(@weak app_grid => @default-return None, move |args| {
let new_filter = args[1].get::<CustomFilter>().unwrap();
app_grid.set_group_filter(&new_filter);
None
}),
);
entry.connect_changed(
glib::clone!(@weak app_grid => move |search: &gtk4::SearchEntry| {
let search_text = search.text().to_string().to_lowercase();
let new_filter: gtk4::CustomFilter = gtk4::CustomFilter::new(move |obj| {
let search_res = obj.downcast_ref::<gio::DesktopAppInfo>()
.expect("The Object needs to be of type AppInfo");
search_res.name().to_string().to_lowercase().contains(&search_text)
});
let search_text = search.text().to_string().to_lowercase();
let new_sorter: gtk4::CustomSorter = gtk4::CustomSorter::new(move |obj1, obj2| {
let app_info1 = obj1.downcast_ref::<gio::DesktopAppInfo>().unwrap();
let app_info2 = obj2.downcast_ref::<gio::DesktopAppInfo>().unwrap();
if search_text == "" {
return app_info1
.name()
.to_lowercase()
.cmp(&app_info2.name().to_lowercase())
.into();
}
let i_1 = app_info1.name().to_lowercase().find(&search_text);
let i_2 = app_info2.name().to_lowercase().find(&search_text);
match (i_1, i_2) {
(Some(i_1), Some(i_2)) => i_1.cmp(&i_2).into(),
(Some(_), None) => std::cmp::Ordering::Less.into(),
(None, Some(_)) => std::cmp::Ordering::Greater.into(),
_ => app_info1
.name()
.to_lowercase()
.cmp(&app_info2.name().to_lowercase())
.into()
}
});
app_grid.set_search_filter(&new_filter);
app_grid.set_app_sorter(&new_sorter);
}),
);
}
}

View file

@ -1,46 +0,0 @@
use glib::subclass::Signal;
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use once_cell::sync::Lazy;
use std::cell::RefCell;
use std::rc::Rc;
use crate::dock_popover::DockPopover;
#[derive(Debug, Default)]
pub struct DockItem {
pub image: Rc<RefCell<Option<gtk4::Image>>>,
pub dots: Rc<RefCell<gtk4::Label>>,
pub item_box: Rc<RefCell<gtk4::Box>>,
pub popover: Rc<RefCell<gtk4::Popover>>,
pub popover_menu: Rc<RefCell<DockPopover>>,
}
#[glib::object_subclass]
impl ObjectSubclass for DockItem {
const NAME: &'static str = "DockItem";
type Type = super::DockItem;
type ParentType = gtk4::Button;
}
impl ObjectImpl for DockItem {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder(
// Signal name
"popover-closed",
// Types of the values which will be sent to the signal handler
&[],
// Type of the value the signal handler sends back
<()>::static_type().into(),
)
.build()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for DockItem {}
impl ButtonImpl for DockItem {}

View file

@ -1,137 +0,0 @@
use cascade::cascade;
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::Align;
use gtk4::Box;
use gtk4::Image;
use gtk4::Label;
use gtk4::Orientation;
use gtk4::Popover;
use crate::dock_object::DockObject;
use crate::dock_popover::DockPopover;
use crate::utils::BoxedWindowList;
mod imp;
glib::wrapper! {
pub struct DockItem(ObjectSubclass<imp::DockItem>)
@extends gtk4::Button, gtk4::Widget,
@implements gtk4::Accessible, gtk4::Actionable, gtk4::Buildable, gtk4::ConstraintTarget;
}
impl Default for DockItem {
fn default() -> Self {
Self::new()
}
}
impl DockItem {
pub fn new() -> Self {
let self_: DockItem = glib::Object::new(&[]).expect("Failed to create DockItem");
let item_box = Box::new(Orientation::Vertical, 0);
cascade! {
&self_;
..set_child(Some(&item_box));
..add_css_class("dock_item");
};
let image = cascade! {
Image::new();
..set_hexpand(true);
..set_halign(Align::Center);
..set_pixel_size(64);
};
let dots = cascade! {
Label::new(Some(""));
..set_hexpand(true);
..set_halign(Align::Center);
};
item_box.append(&image);
item_box.append(&dots);
let popover = cascade! {
Popover::new();
..set_autohide(true);
};
item_box.append(&popover);
let self_clone = self_.clone();
popover.connect_closed(move |_| {
let _ = self_clone.emit_by_name::<()>("popover-closed", &[]);
});
let popover_menu = cascade! {
DockPopover::new();
};
popover.set_child(Some(&popover_menu));
popover_menu.connect_local(
"menu-hide",
false,
glib::clone!(@weak popover, @weak popover_menu => @default-return None, move |_| {
popover.popdown();
popover_menu.reset_menu();
None
}),
);
let imp = imp::DockItem::from_instance(&self_);
imp.image.replace(Some(image));
imp.dots.replace(dots);
imp.item_box.replace(item_box);
imp.popover.replace(popover);
imp.popover_menu.replace(popover_menu);
self_
}
// refactor to emit event for removing the item?
pub fn set_dock_object(&self, dock_object: &DockObject) {
let self_ = imp::DockItem::from_instance(self);
let image = cascade! {
dock_object.get_image();
..set_hexpand(true);
..set_halign(Align::Center);
..set_pixel_size(64);
..set_tooltip_text(dock_object.get_name().as_deref());
};
let old_image = self_.image.replace(None);
if let Some(old_image) = old_image {
self_.item_box.borrow().remove(&old_image);
self_.item_box.borrow().prepend(&image);
self_.image.replace(Some(image));
}
let active = dock_object.property::<BoxedWindowList>("active");
let dots = self_.dots.borrow();
dots.set_text("");
for _ in active.0 {
dots.set_text(format!("{}{}", dots.text(), " · ").as_str());
}
let popover = dock_object.property::<bool>("popover");
// dbg!(popover);
// dbg!(dock_object);
if popover {
self.add_popover(dock_object);
} else {
self.clear_popover();
}
}
pub fn add_popover(&self, item: &DockObject) {
let imp = imp::DockItem::from_instance(self);
let popover = imp.popover.borrow();
let popover_menu = imp.popover_menu.borrow();
popover_menu.set_dock_object(item, true);
popover.popup();
}
pub fn clear_popover(&self) {
let imp = imp::DockItem::from_instance(self);
let popover = imp.popover.borrow();
let popover_menu = imp.popover_menu.borrow();
popover.popdown();
popover_menu.reset_menu();
}
}

View file

@ -1,34 +0,0 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use glib::SignalHandlerId;
use gtk4::subclass::prelude::*;
use gtk4::{gio, glib};
use gtk4::{Box, DragSource, DropTarget, GestureClick, ListView};
use once_cell::sync::OnceCell;
#[derive(Debug, Default)]
pub struct DockList {
pub list_view: OnceCell<ListView>,
pub type_: OnceCell<super::DockListType>,
pub model: OnceCell<gio::ListStore>,
pub click_controller: OnceCell<GestureClick>,
pub drop_controller: OnceCell<DropTarget>,
pub drag_source: OnceCell<DragSource>,
pub drag_end_signal: Rc<RefCell<Option<SignalHandlerId>>>,
pub drag_cancel_signal: Rc<RefCell<Option<SignalHandlerId>>>,
pub popover_menu_index: Rc<Cell<Option<u32>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for DockList {
const NAME: &'static str = "DockList";
type Type = super::DockList;
type ParentType = Box;
}
impl ObjectImpl for DockList {}
impl WidgetImpl for DockList {}
impl BoxImpl for DockList {}

View file

@ -1,627 +0,0 @@
use crate::dock_item::DockItem;
use crate::dock_object::DockObject;
use crate::plugin;
use crate::utils::data_path;
use crate::BoxedWindowList;
use crate::Event;
use crate::Item;
use crate::PLUGINS;
use crate::TX;
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::glib;
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::{DragSource, GestureClick};
use std::ffi::CStr;
use std::fs::File;
use std::io::BufReader;
use std::io::Read;
use std::path::Path;
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")
}
pub fn popover_index(&self) -> Option<u32> {
// Get state
let imp = imp::DockList::from_instance(self);
imp.popover_menu_index.get()
}
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);
}
} else {
eprintln!("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);
}
}
}
}
})
}
});
}
// TODO load saved plugins here... for now, load the hardcoded example.
// TODO unload plugin library before the dynamic library is changed, otherwise, it will crash after segfault
// TODO unload plugin on removal from model
// TODO dnd for plugin? I think they should either be at the start or end of the dock and not draggable
// TODO call plugin click handler on click or if it is not provided by the library, open the popover menu instead
let mut path_dir = glib::user_data_dir();
path_dir.push(crate::ID);
std::fs::create_dir_all(&path_dir).expect("Could not create directory.");
path_dir.push("plugins");
std::fs::create_dir_all(&path_dir).expect("Could not create directory.");
let mut path = path_dir.clone();
path.push("dock_plugin_uwu.so");
let mut path_css = path_dir.clone();
path_css.push("dock_plugin_uwu.css");
let provider = gtk4::CssProvider::new();
if path.exists() {
let path = path
.as_os_str()
.to_str()
.expect("plugin path needs to be a valid string");
if let Ok(f) = File::open(path_css) {
let mut reader = BufReader::new(f);
let mut buffer = Vec::new();
if reader.read_to_end(&mut buffer).is_ok() {
provider.load_from_data(&buffer);
// Add the provider to the default screen
gtk4::StyleContext::add_provider_for_display(
&gdk4::Display::default().expect("Error initializing GTK CSS provider."),
&provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
} else {
eprintln!("loading plugin css failed");
}
} else {
eprintln!("loading plugin css failed");
}
let (popover_menu, image, name, lib) = unsafe {
let lib = libloading::Library::new(path).unwrap();
// store library until unloading the plugin
let image_func: libloading::Symbol<
unsafe extern "C" fn() -> *mut gtk4_sys::GtkWidget,
> = lib.get(b"dock_plugin_image").unwrap();
let popover_func: libloading::Symbol<
unsafe extern "C" fn() -> *mut gtk4_sys::GtkWidget,
> = lib.get(b"dock_plugin_popover_menu").unwrap();
let name_func: libloading::Symbol<
unsafe extern "C" fn() -> *const std::os::raw::c_char,
> = lib.get(b"dock_plugin_name").unwrap();
// click handler is optional
(popover_func(), image_func(), name_func(), lib)
};
if let Ok(ref mut mutex) = PLUGINS.try_lock() {
mutex.insert(String::from(path), lib);
}
let name = if !name.is_null() {
unsafe { String::from(CStr::from_ptr(name).to_str().unwrap_or_default()) }
} else {
String::new()
};
let image = if !image.is_null() {
unsafe {
gtk4::glib::translate::from_glib_none::<_, gtk4::Widget>(image).unsafe_cast()
}
} else {
gtk4::Image::new()
};
let popover_menu = if !popover_menu.is_null() {
unsafe {
gtk4::glib::translate::from_glib_none::<_, gtk4::Widget>(popover_menu)
.unsafe_cast()
}
} else {
gtk4::Box::new(Orientation::Vertical, 4)
};
let boxed_plugin = plugin::BoxedDockPlugin {
path: String::from(path),
name,
image,
popover_menu,
};
let model = self.model();
model.append(&DockObject::from_plugin(boxed_plugin).upcast::<Object>());
}
}
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 Some(app_info) = dock_object.property::<Option<DesktopAppInfo>>("appinfo") {
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");
// TODO save plugins here for now examples are hardcoded and don't need to be saved
}
fn layout(&self) {
let imp = imp::DockList::from_instance(&self);
let list_view = cascade! {
ListView::default();
..set_orientation(Orientation::Horizontal);
..add_css_class("docklist");
};
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();
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| {
let window = list_view.root().unwrap().downcast::<Window>().unwrap();
let max_x = list_view.allocated_width();
let max_y = list_view.allocated_height();
// dbg!(max_y);
// dbg!(y);
let n_buckets = model.n_items();
let index = (x * n_buckets as f64 / (max_x as f64 + 0.1)) as u32;
// dbg!(self_.current_button());
// dbg!(self_.last_event(self_.current_sequence().as_ref()));
let click_modifier = if let Some(event) = self_.last_event(self_.current_sequence().as_ref()) {
// dbg!(&event);
Some(event.modifier_state())
}
else {
None
};
// dbg!(click_modifier);
// 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;
}
});
};
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>() {
old_dock_object.set_popover(false);
popover_menu_index.replace(None);
model.items_changed(old_index, 0, 0);
//TODO signal dock to check if it should hide
}
}
return;
}
if y > f64::from(max_y) || y < 0.0 || x > f64::from(max_x) || x < 0.0 {
// println!("out of bounds click...");
return;
}
if let Some(item) = model.item(index) {
if let Ok(dock_object) = item.downcast::<DockObject>() {
let active = dock_object.property::<BoxedWindowList>("active");
let app_info = dock_object.property::<Option<DesktopAppInfo>>("appinfo");
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");
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);
}
_ => eprintln!("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));
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 Some(cur_app_info) = cur_app_info.property::<Option<DesktopAppInfo>>("appinfo") {
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| {
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 Some(app_info) = dock_object.property::<Option<DesktopAppInfo>>("appinfo") {
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() {
let icon_theme = IconTheme::for_display(default_display);
let 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
}));
// TODO investigate why drop does not finish when dropping on some surfaces
// for now this is a fix that will cancel the drop after 100 ms and not completing.
drag_source.connect_drag_begin(|_self, drag| {
drag.connect_drop_performed(|_self| {
glib::timeout_add_local_once(
std::time::Duration::from_millis(100),
glib::clone!(@weak _self => move || {
_self.drop_done(false);
}),
);
});
});
imp.drag_source
.set(drag_source)
.expect("Could not set saved drag source");
}
fn setup_factory(&self) {
let imp = imp::DockList::from_instance(self);
let popover_menu_index = &imp.popover_menu_index;
let factory = SignalListItemFactory::new();
let model = imp.model.get().expect("Failed to get saved app model.");
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
});
list_item.set_child(Some(&dock_item));
}),
);
factory.connect_bind(move |_, list_item| {
let dock_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_dock_object(&dock_object);
});
// Set the factory of the list view
imp.list_view.get().unwrap().set_factory(Some(&factory));
}
}

View file

@ -1,114 +0,0 @@
use std::cell::Cell;
use std::cell::RefCell;
use gdk4::glib::ParamSpecBoolean;
use gdk4::glib::ParamSpecBoxed;
use gdk4::glib::ParamSpecObject;
use gio::DesktopAppInfo;
use glib::{ParamFlags, ParamSpec, Value};
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use once_cell::sync::Lazy;
use crate::plugin::BoxedDockPlugin;
use crate::utils::BoxedWindowList;
// Object holding the state
#[derive(Default)]
pub struct DockObject {
pub(super) appinfo: RefCell<Option<DesktopAppInfo>>,
pub(super) active: RefCell<BoxedWindowList>,
pub(super) plugin: RefCell<Option<BoxedDockPlugin>>,
pub(super) saved: Cell<bool>,
pub(super) popover: Cell<bool>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for DockObject {
const NAME: &'static str = "DockObject";
type Type = super::DockObject;
type ParentType = glib::Object;
}
// Trait shared by all GObjects
impl ObjectImpl for DockObject {
fn properties() -> &'static [ParamSpec] {
static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
vec![
ParamSpecObject::new(
// Name
"appinfo",
// Nickname
"appinfo",
// Short description
"app info",
DesktopAppInfo::static_type(),
// The property can be read and written to
ParamFlags::READWRITE,
),
ParamSpecBoxed::new(
// Name
"active",
// Nickname
"active",
// Short description
"active",
BoxedWindowList::static_type(),
// The property can be read and written to
ParamFlags::READWRITE,
),
ParamSpecBoolean::new(
"saved",
"saved",
"Indicates whether app is saved to the dock",
false,
ParamFlags::READWRITE,
),
ParamSpecBoolean::new(
"popover",
"popover",
"Indicates whether there is a popover menu displayed for this object",
false,
ParamFlags::READWRITE,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
match pspec.name() {
"appinfo" => {
let appinfo = value
.get()
.expect("Value needs to be Option<DesktopAppInfo>");
self.appinfo.replace(appinfo);
}
"active" => {
let active = value.get().expect("Value needs to be BoxedWindowList");
self.active.replace(active);
}
"saved" => {
self.saved
.replace(value.get().expect("Value needs to be a boolean"));
}
"popover" => {
self.popover
.replace(value.get().expect("Value needs to be a boolean"));
}
_ => unimplemented!(),
}
}
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value {
match pspec.name() {
"appinfo" => self.appinfo.borrow().to_value(),
"active" => self.active.borrow().to_value(),
"saved" => self.saved.get().to_value(),
"popover" => self.popover.get().to_value(),
_ => unimplemented!(),
}
}
}

View file

@ -1,141 +0,0 @@
use std::path::Path;
use crate::plugin;
use crate::utils::BoxedWindowList;
use gdk4::glib::Object;
use gdk4::subclass::prelude::ObjectSubclassExt;
use gio::{DesktopAppInfo, Icon};
use gtk4::prelude::*;
use gtk4::{glib, Image};
mod imp;
glib::wrapper! {
pub struct DockObject(ObjectSubclass<imp::DockObject>);
}
impl DockObject {
pub fn new(appinfo: DesktopAppInfo) -> Self {
Object::new(&[("appinfo", &Some(appinfo)), ("saved", &true)])
.expect("Failed to create `DockObject`.")
}
pub fn from_app_info_path(path: &str) -> Option<Self> {
if let Some(path) = Path::new(path).file_name() {
if let Some(path) = path.to_str() {
if let Some(appinfo) = gio::DesktopAppInfo::new(path) {
if appinfo.should_show() {
return Some(
Object::new(&[("appinfo", &Some(appinfo)), ("saved", &true)])
.expect("Failed to create `DockObject`."),
);
}
}
}
}
None
}
pub fn from_plugin(plugin: plugin::BoxedDockPlugin) -> Self {
let self_ = Object::new(&[("saved", &true)]).expect("Failed to create `DockObject`.");
let imp = imp::DockObject::from_instance(&self_);
imp.plugin.replace(Some(plugin));
self_
}
pub fn get_path(&self) -> Option<String> {
let imp = imp::DockObject::from_instance(&self);
if let Some(app_info) = imp.appinfo.borrow().as_ref() {
app_info
.filename()
.map(|name| name.to_string_lossy().into())
} else if let Some(plugin) = imp.plugin.borrow().as_ref() {
Some(plugin.path.clone())
} else {
None
}
}
pub fn get_name(&self) -> Option<String> {
let imp = imp::DockObject::from_instance(&self);
if let Some(app_info) = imp.appinfo.borrow().as_ref() {
Some(app_info.name().to_string())
} else if let Some(plugin) = imp.plugin.borrow().as_ref() {
Some(plugin.name.clone())
} else {
None
}
}
pub fn get_popover_menu(&self) -> Option<gtk4::Box> {
let imp = imp::DockObject::from_instance(&self);
if let Some(plugin) = imp.plugin.borrow().as_ref() {
Some(plugin.popover_menu.clone())
} else {
None
}
}
pub fn get_image(&self) -> gtk4::Image {
let imp = imp::DockObject::from_instance(&self);
if let Some(app_info) = imp.appinfo.borrow().as_ref() {
let image = Image::new();
let icon = app_info
.icon()
.unwrap_or(Icon::for_string("image-missing").expect("Failed to set default icon"));
image.set_from_gicon(&icon);
image
} else if let Some(plugin) = imp.plugin.borrow().as_ref() {
plugin.image.clone()
} else {
println!("failed to load image");
Image::new()
}
}
pub fn set_saved(&self, is_saved: bool) {
let imp = imp::DockObject::from_instance(&self);
imp.saved.replace(is_saved);
}
pub fn from_search_results(results: BoxedWindowList) -> Self {
let appinfo = if let Some(first) = results.0.iter().next() {
xdg::BaseDirectories::new()
.expect("could not access XDG Base directory")
.get_data_dirs()
.iter_mut()
.filter_map(|xdg_data_path| {
xdg_data_path.push("applications");
std::fs::read_dir(xdg_data_path).ok()
})
.flatten()
.filter_map(|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()
&& first.description.as_str() == app_info.name().as_str()
{
return Some(app_info);
}
}
}
}
}
None
})
.next()
} else {
None
};
// dbg!(&appinfo);
Object::new(&[("appinfo", &appinfo), ("active", &results)])
.expect("Failed to create `DockObject`.")
}
pub fn set_popover(&self, b: bool) {
let imp = imp::DockObject::from_instance(self);
imp.popover.replace(b);
}
}

View file

@ -1,52 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use glib::subclass::Signal;
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::{Box, Button, ListBox, Revealer};
use once_cell::sync::Lazy;
use crate::dock_object::DockObject;
#[derive(Debug, Default)]
pub struct DockPopover {
pub menu_handle: Rc<RefCell<Box>>,
pub all_windows_item_revealer: Rc<RefCell<Revealer>>,
pub all_windows_item_header: Rc<RefCell<Button>>,
pub window_list: Rc<RefCell<ListBox>>,
pub launch_new_item: Rc<RefCell<Button>>,
pub favorite_item: Rc<RefCell<Button>>,
pub quit_all_item: Rc<RefCell<Button>>,
//TODO figure out how to use lifetimes with glib::wrapper! macro
pub dock_object: Rc<RefCell<Option<DockObject>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for DockPopover {
const NAME: &'static str = "DockPopover";
type Type = super::DockPopover;
type ParentType = Box;
}
impl ObjectImpl for DockPopover {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder(
// Signal name
"menu-hide",
// Types of the values which will be sent to the signal handler
&[],
// Type of the value the signal handler sends back
<()>::static_type().into(),
)
.build()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for DockPopover {}
impl BoxImpl for DockPopover {}

View file

@ -1,253 +0,0 @@
use cascade::cascade;
use gdk4::pango::EllipsizeMode;
use gio::DesktopAppInfo;
use gtk4::subclass::prelude::*;
use gtk4::{gio, glib};
use gtk4::{prelude::*, Label};
use gtk4::{Box, Button, Image, ListBox, Orientation, Window};
use crate::dock_object::DockObject;
use crate::utils::BoxedWindowList;
use crate::Event;
use crate::TX;
mod imp;
glib::wrapper! {
pub struct DockPopover(ObjectSubclass<imp::DockPopover>)
@extends gtk4::Widget, gtk4::Box,
@implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Orientable;
}
impl Default for DockPopover {
fn default() -> Self {
Self::new()
}
}
impl DockPopover {
pub fn new() -> Self {
let self_: DockPopover = glib::Object::new(&[]).expect("Failed to create DockList");
self_.layout();
//dnd behavior is different for each type, as well as the data in the model
self_
}
pub fn set_dock_object(&self, dock_object: &DockObject, update_layout: bool) {
let imp = imp::DockPopover::from_instance(&self);
imp.dock_object.replace(Some(dock_object.clone()));
if update_layout {
self.update_layout();
}
}
pub fn update_layout(&self) {
self.reset_menu();
cascade! {
&self;
..set_spacing(4);
..set_orientation(Orientation::Vertical);
..set_hexpand(true);
};
// build menu
let imp = imp::DockPopover::from_instance(&self);
let dock_object = imp.dock_object.borrow();
let menu_handle = imp.menu_handle.borrow();
if let Some(dock_object) = dock_object.as_ref() {
if let Some(menu) = dock_object.get_popover_menu() {
menu_handle.append(&menu);
} else {
let all_windows_item_container = cascade! {
Box::new(Orientation::Vertical, 4);
};
menu_handle.append(&all_windows_item_container);
let window_list = dock_object.property::<BoxedWindowList>("active");
if window_list.0.len() == 0 {
all_windows_item_container.hide();
} else {
let window_listbox = cascade! {
ListBox::new();
..set_activate_on_single_click(true);
};
all_windows_item_container.append(&window_listbox);
for w in window_list.0 {
let window_box = cascade! {
Box::new(Orientation::Vertical, 4);
};
window_listbox.append(&window_box);
let window_title = cascade! {
Label::new(Some(w.name.as_str()));
..set_margin_start(4);
..set_margin_end(4);
..set_margin_top(4);
..set_margin_bottom(4);
..set_wrap(true);
..set_max_width_chars(20);
..set_ellipsize(EllipsizeMode::End);
..add_css_class("title-4");
..add_css_class("window_title");
};
let window_image = cascade! {
//TODO fill with image of window
Image::from_pixbuf(None);
};
window_box.append(&window_image);
window_box.append(&window_title);
}
// imp.all_windows_item_revealer.replace(window_list_revealer);
imp.window_list.replace(window_listbox);
}
let launch_item_container = cascade! {
Box::new(Orientation::Vertical, 4);
..set_hexpand(true);
};
menu_handle.append(&launch_item_container);
let launch_new_item = cascade! {
Button::with_label("New Window");
};
launch_item_container.append(&launch_new_item);
imp.launch_new_item.replace(launch_new_item);
let favorite_item = cascade! {
Button::with_label(if dock_object.property::<bool>("saved") {"Remove from Favorites"} else {"Add to Favorites"});
};
menu_handle.append(&favorite_item);
imp.favorite_item.replace(favorite_item);
let window_list = dock_object.property::<BoxedWindowList>("active");
if window_list.0.len() > 1 {
let quit_all_item = cascade! {
Button::with_label(format!("Quit {} Windows", window_list.0.len()).as_str());
};
menu_handle.append(&quit_all_item);
imp.quit_all_item.replace(quit_all_item);
} else {
let quit_all_item = cascade! {
Button::with_label("Quit");
};
menu_handle.append(&quit_all_item);
if window_list.0.len() == 0 {
quit_all_item.hide();
}
imp.quit_all_item.replace(quit_all_item);
}
self.setup_handlers();
}
}
}
fn layout(&self) {
let imp = imp::DockPopover::from_instance(&self);
let menu_handle = cascade! {
Box::new(Orientation::Vertical, 4);
};
self.append(&menu_handle);
imp.menu_handle.replace(menu_handle);
}
fn emit_hide(&self) {
self.emit_by_name::<()>("menu-hide", &[]);
}
pub fn reset_menu(&self) {
// reset menu
let menu_handle = cascade! {
Box::new(Orientation::Vertical, 4);
};
self.append(&menu_handle);
let imp = imp::DockPopover::from_instance(&self);
let old_menu_handle = imp.menu_handle.replace(menu_handle);
self.remove(&old_menu_handle);
}
fn setup_handlers(&self) {
let imp = imp::DockPopover::from_instance(&self);
let dock_object = imp.dock_object.borrow();
let launch_new_item = imp.launch_new_item.borrow();
let favorite_item = imp.favorite_item.borrow();
let quit_all_item = imp.quit_all_item.borrow();
let window_listbox = imp.window_list.borrow();
// let all_windows_header = imp.all_windows_item_header.borrow();
// let revealer = &imp.all_windows_item_revealer;
if let Some(dock_object) = dock_object.as_ref() {
// println!("setting up popover menu handlers");
let self_ = self.clone();
launch_new_item.connect_clicked(glib::clone!(@weak dock_object, => move |_| {
let app_info = dock_object.property::<Option<DesktopAppInfo>>("appinfo").expect("Failed to convert value to DesktopAppInfo");
let window = self_.root().unwrap().downcast::<Window>().unwrap();
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();
}
self_.emit_hide();
}));
let self_ = self.clone();
quit_all_item.connect_clicked(glib::clone!(@weak dock_object => move |_| {
let active = dock_object.property::<BoxedWindowList>("active").0;
for w in active {
let entity = w.entity.clone();
glib::MainContext::default().spawn_local(async move {
if let Some(tx) = TX.get() {
let _ = tx.send(Event::Close(entity)).await;
}
});
}
self_.emit_hide();
}));
let self_ = self.clone();
favorite_item.connect_clicked(glib::clone!(@weak dock_object => move |_| {
let saved = dock_object.property::<bool>("saved");
glib::MainContext::default().spawn_local(async move {
if let Some(tx) = TX.get() {
if let Some(name) = dock_object.get_name() {
let _ = tx.send(Event::Favorite((name.into(), !saved))).await;
}
}
});
self_.emit_hide();
}));
// all_windows_header.connect_clicked(
// glib::clone!(@weak dock_object, @weak revealer => move |self_| {
// // dbg!(dock_object);
// let revealer = revealer.borrow();
// revealer.set_reveal_child(!revealer.reveals_child())
// }),
// );
let self_ = self.clone();
window_listbox.connect_row_activated(
glib::clone!(@weak dock_object => move |_, item| {
let active = dock_object.property::<BoxedWindowList>("active").0;
let entity = active[usize::try_from(item.index()).unwrap()].entity.clone();
glib::MainContext::default().spawn_local(async move {
if let Some(tx) = TX.get() {
let _ = tx.send(Event::Activate(entity)).await;
}
});
self_.emit_hide();
}),
);
}
}
}

View file

@ -1,349 +0,0 @@
use std::collections::BTreeMap;
use std::sync::Mutex;
use std::time::Duration;
use crate::dock_list::DockListType;
use crate::utils::{block_on, BoxedWindowList};
use gdk4::Display;
use gio::DesktopAppInfo;
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::Application;
use gtk4::CssProvider;
use gtk4::StyleContext;
use once_cell::sync::{Lazy, OnceCell};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::sync::mpsc;
use zbus::Connection;
use zvariant_derive::Type;
use self::dock_object::DockObject;
use self::window::Window;
mod dock_item;
mod dock_list;
mod dock_object;
mod dock_popover;
mod plugin;
mod utils;
mod window;
const ID: &str = "com.cosmic.dock";
const DEST: &str = "com.System76.PopShell";
const PATH: &str = "/com/System76/PopShell";
static TX: OnceCell<mpsc::Sender<Event>> = OnceCell::new();
static PLUGINS: Lazy<Mutex<HashMap<String, libloading::Library>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
pub enum Event {
WindowList(Vec<Item>),
Activate((u32, u32)),
Close((u32, u32)),
Favorite((String, bool)),
RefreshFromCache,
}
#[derive(Debug, Deserialize, Serialize, Type, Clone, PartialEq, Eq)]
pub struct Item {
entity: (u32, u32),
name: String,
description: String,
desktop_entry: String,
}
fn spawn_zbus(tx: mpsc::Sender<Event>) -> Connection {
let connection = block_on(Connection::session()).unwrap();
let sender = tx.clone();
let conn = connection.clone();
let _ = std::thread::spawn(move || {
let cached_results: Vec<Item> = vec![];
block_on(async move {
futures::pin_mut!(cached_results);
loop {
let m = conn
.call_method(Some(DEST), PATH, Some(DEST), "WindowList", &())
.await;
if let Ok(m) = m {
if let Ok(mut reply) = m.body::<Vec<Item>>() {
let mut cached_results = cached_results.as_mut();
reply.sort_by(|a, b| a.name.cmp(&b.name));
if cached_results.len() != reply.len()
|| !reply.iter().zip(cached_results.iter()).fold(
0,
|acc, z: (&Item, &Item)| {
let (a, b) = z;
if a.name == b.name {
acc + 1
} else {
acc
}
},
) == cached_results.len()
{
cached_results.splice(.., reply.clone());
let _ = sender.send(Event::WindowList(reply)).await;
}
}
glib::timeout_future(Duration::from_millis(100)).await;
}
}
})
});
connection
}
fn _setup_shortcuts(_app: &Application) {}
fn load_css() {
// Load the css file and add it to the provider
let provider = CssProvider::new();
provider.load_from_data(include_bytes!("style.css"));
// Add the provider to the default screen
StyleContext::add_provider_for_display(
&Display::default().expect("Error initializing GTK CSS provider."),
&provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn main() {
assert!(utils::BoxedWindowList::static_type().is_valid());
assert!(plugin::BoxedDockPlugin::static_type().is_valid());
let app = gtk4::Application::builder().application_id(ID).build();
app.connect_startup(|_app| {
// setup_shortcuts(app);
load_css()
});
app.connect_activate(move |app| {
let (tx, mut rx) = mpsc::channel(100);
let zbus_conn = spawn_zbus(tx.clone());
if TX.set(tx).is_err() {
eprintln!("failed to set global Sender. Exiting");
std::process::exit(1);
};
let window = Window::new(app);
window.show();
let cached_results: Vec<Item> = vec![];
glib::MainContext::default().spawn_local(async move {
futures::pin_mut!(cached_results);
// let rx = RX.get().unwrap().clone();
while let Some(event) = rx.recv().await {
match event {
Event::Activate(e) => {
let _activate_window = zbus_conn
.call_method(Some(DEST), PATH, Some(DEST), "WindowFocus", &((e,)))
.await
.expect("Failed to focus selected window");
}
Event::Close(e) => {
let _activate_window = zbus_conn
.call_method(Some(DEST), PATH, Some(DEST), "WindowQuit", &((e,)))
.await
.expect("Failed to close selected window");
}
Event::Favorite((name, should_favorite)) => {
dbg!(&name);
dbg!(should_favorite);
let saved_app_model = window.model(DockListType::Saved);
let active_app_model = window.model(DockListType::Active);
if should_favorite {
let mut cur: u32 = 0;
let mut index: Option<u32> = None;
while let Some(item) = active_app_model.item(cur) {
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
if cur_dock_object.get_path() == Some(name.clone()) {
cur_dock_object.set_saved(true);
index = Some(cur);
}
}
cur += 1;
}
if let Some(index) = index {
let object = active_app_model.item(index).unwrap();
active_app_model.remove(index);
saved_app_model.append(&object);
}
} else {
let mut cur: u32 = 0;
let mut index: Option<u32> = None;
while let Some(item) = saved_app_model.item(cur) {
if let Ok(cur_dock_object) = item.downcast::<DockObject>() {
if cur_dock_object.get_path() == Some(name.clone()) {
cur_dock_object.set_saved(false);
index = Some(cur);
}
}
cur += 1;
}
if let Some(index) = index {
let object = saved_app_model.item(index).unwrap();
saved_app_model.remove(index);
active_app_model.append(&object);
}
}
let _ = TX.get().unwrap().send(Event::RefreshFromCache).await;
}
Event::RefreshFromCache => {
// println!("refreshing model from cache");
let cached_results = cached_results.as_ref();
let stack_active = cached_results.iter().fold(
BTreeMap::new(),
|mut acc: BTreeMap<String, BoxedWindowList>, elem| {
if let Some(v) = acc.get_mut(&elem.description) {
v.0.push(elem.clone());
} else {
acc.insert(
elem.description.clone(),
BoxedWindowList(vec![elem.clone()]),
);
}
acc
},
);
let mut stack_active: Vec<BoxedWindowList> =
stack_active.into_values().collect();
// update active app stacks for saved apps into the saved app model
// then put the rest in the active app model (which doesn't include saved apps)
let saved_app_model = window.model(DockListType::Saved);
let mut saved_i: u32 = 0;
while let Some(item) = saved_app_model.item(saved_i) {
if let Ok(dock_obj) = item.downcast::<DockObject>() {
if let Some(cur_app_info) =
dock_obj.property::<Option<DesktopAppInfo>>("appinfo")
{
if let Some((i, _s)) = stack_active
.iter()
.enumerate()
.find(|(_i, s)| s.0[0].description == cur_app_info.name())
{
// println!(
// "found active saved app {} at {}",
// _s.0[0].name, i
// );
let active = stack_active.remove(i);
dock_obj.set_property("active", active.to_value());
saved_app_model.items_changed(
saved_i.try_into().unwrap(),
0,
0,
);
} else if let Some(_) = cached_results
.iter()
.find(|s| s.description == cur_app_info.name())
{
dock_obj.set_property(
"active",
BoxedWindowList(Vec::new()).to_value(),
);
saved_app_model.items_changed(
saved_i.try_into().unwrap(),
0,
0,
);
}
}
}
saved_i += 1;
}
let active_app_model = window.model(DockListType::Active);
let model_len = active_app_model.n_items();
let new_results: Vec<glib::Object> = stack_active
.into_iter()
.map(|v| DockObject::from_search_results(v).upcast())
.collect();
active_app_model.splice(0, model_len, &new_results[..]);
}
Event::WindowList(results) => {
// sort to make comparison with cache easier
let mut cached_results = cached_results.as_mut();
// build active app stacks for each app
let stack_active = results.iter().fold(
BTreeMap::new(),
|mut acc: BTreeMap<String, BoxedWindowList>, elem| {
if let Some(v) = acc.get_mut(&elem.description) {
v.0.push(elem.clone());
} else {
acc.insert(
elem.description.clone(),
BoxedWindowList(vec![elem.clone()]),
);
}
acc
},
);
let mut stack_active: Vec<BoxedWindowList> =
stack_active.into_values().collect();
// update active app stacks for saved apps into the saved app model
// then put the rest in the active app model (which doesn't include saved apps)
let saved_app_model = window.model(DockListType::Saved);
let mut saved_i: u32 = 0;
while let Some(item) = saved_app_model.item(saved_i) {
if let Ok(dock_obj) = item.downcast::<DockObject>() {
if let Some(cur_app_info) =
dock_obj.property::<Option<DesktopAppInfo>>("appinfo")
{
if let Some((i, _s)) = stack_active
.iter()
.enumerate()
.find(|(_i, s)| s.0[0].description == cur_app_info.name())
{
// println!("found active saved app {} at {}", s.0[0].name, i);
let active = stack_active.remove(i);
dock_obj.set_property("active", active.to_value());
saved_app_model.items_changed(
saved_i.try_into().unwrap(),
0,
0,
);
} else if let Some(_) = cached_results
.iter()
.find(|s| s.description == cur_app_info.name())
{
dock_obj.set_property(
"active",
BoxedWindowList(Vec::new()).to_value(),
);
saved_app_model.items_changed(
saved_i.try_into().unwrap(),
0,
0,
);
}
}
}
saved_i += 1;
}
let active_app_model = window.model(DockListType::Active);
let model_len = active_app_model.n_items();
let new_results: Vec<glib::Object> = stack_active
.into_iter()
.map(|v| DockObject::from_search_results(v).upcast())
.collect();
active_app_model.splice(0, model_len, &new_results[..]);
cached_results.splice(.., results);
}
}
}
});
});
app.run();
}

View file

@ -1,10 +0,0 @@
use gtk4::glib;
#[derive(Clone, Debug, Default, gtk4::glib::Boxed)]
#[boxed_type(name = "BoxedDockPlugin")]
pub struct BoxedDockPlugin {
pub path: String,
pub name: String,
pub image: gtk4::Image,
pub popover_menu: gtk4::Box,
}

View file

@ -1,69 +0,0 @@
listview.docklist {
border-radius: 12px;
background: transparent;
transition: 100ms;
}
listview.docklist row:hover {
background: #888888CC;
border-radius: 12px;
padding: 0px;
}
listview.docklist row {
transition: 100ms;
background: transparent;
border-radius: 12px;
padding: 0px;
}
button:hover {
transition: 300ms;
border-width: 0px;
background: #888888CC;
border-radius: 12px;
}
button {
transition: 300ms;
background: transparent;
border-radius: 12px;
border-width: 0px;
}
list {
border-radius: 12px;
background: #333333CC;
}
label.window_title {
color: white;
margin: 4px;
}
popover contents {
border-width: 0px;
border-radius: 12px;
padding: 12px;
background: #333333DD;
}
popover arrow {
border-width: 0px;
border-radius: 12px;
padding: 12px;
background: #333333DD;
}
box.dock {
border-radius: 12px;
background: #333333CC;
}
image.dock {
border-radius: 12px;
}
window.root_window {
background: transparent;
}

View file

@ -1,38 +0,0 @@
use std::path::PathBuf;
use gtk4::glib;
use std::future::Future;
use crate::DockObject;
use crate::Item;
#[derive(Clone, Debug, Default, glib::Boxed)]
#[boxed_type(name = "BoxedWindowList")]
pub struct BoxedWindowList(pub Vec<Item>);
#[derive(Clone, Debug, Default, glib::Boxed)]
#[boxed_type(name = "BoxedDockObject")]
pub struct BoxedDockObject(pub Option<DockObject>);
pub fn data_path() -> PathBuf {
let mut path = glib::user_data_dir();
path.push(crate::ID);
std::fs::create_dir_all(&path).expect("Could not create directory.");
path.push("data.json");
path
}
pub fn thread_context() -> glib::MainContext {
glib::MainContext::thread_default().unwrap_or_else(|| {
let ctx = glib::MainContext::new();
ctx
})
}
pub fn block_on<F>(future: F) -> F::Output
where
F: Future,
{
let ctx = thread_context();
ctx.with_thread_default(|| ctx.block_on(future)).unwrap()
}

View file

@ -1,41 +0,0 @@
use gtk4::glib;
use gtk4::subclass::prelude::*;
use gtk4::Box;
use gtk4::DropTarget;
use gtk4::EventControllerMotion;
use gtk4::Revealer;
use once_cell::sync::OnceCell;
use crate::dock_list::DockList;
// Object holding the state
#[derive(Default)]
pub struct Window {
pub revealer: OnceCell<Revealer>,
pub cursor_handle: OnceCell<Box>,
pub cursor_motion_controller: OnceCell<EventControllerMotion>,
pub window_drop_controller: OnceCell<DropTarget>,
pub saved_list: OnceCell<DockList>,
pub active_list: OnceCell<DockList>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "DockWindow";
type Type = super::Window;
type ParentType = gtk4::ApplicationWindow;
}
// Trait shared by all GObjects
impl ObjectImpl for Window {}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {}
// Trait shared by all application
impl ApplicationWindowImpl for Window {}

View file

@ -1,252 +0,0 @@
use cascade::cascade;
use gdk4_x11::X11Display;
use glib::Object;
use glib::Type;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::Align;
use gtk4::Application;
use gtk4::Box;
use gtk4::DropTarget;
use gtk4::EventControllerMotion;
use gtk4::Orientation;
use gtk4::Revealer;
use gtk4::RevealerTransitionType;
use gtk4::Separator;
use gtk4::{gio, glib};
use libcosmic::x;
use crate::dock_list::DockList;
use crate::dock_list::DockListType;
mod imp;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk4::ApplicationWindow, gtk4::Window, gtk4::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Native, gtk4::Root, gtk4::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
let self_: Self = Object::new(&[("application", app)]).expect("Failed to create `Window`.");
let imp = imp::Window::from_instance(&self_);
cascade! {
&self_;
..set_height_request(100);
..set_width_request(128);
..set_title(Some("Cosmic Dock"));
..set_decorated(false);
..set_resizable(false);
..add_css_class("root_window");
};
let cursor_handle = Box::new(Orientation::Vertical, 0);
self_.set_child(Some(&cursor_handle));
let window_filler = cascade! {
Box::new(Orientation::Vertical, 0);
..set_height_request(0); // shrinks to nothing when revealer is shown
..set_vexpand(true); // expands to fill window when revealer is hidden, preventingb window from changing size so much...
};
cursor_handle.append(&window_filler);
let revealer = cascade! {
Revealer::new();
..set_reveal_child(true);
..set_valign(Align::Baseline);
..set_transition_duration(150);
..set_transition_type(RevealerTransitionType::SwingUp);
};
cursor_handle.append(&revealer);
let dock = cascade! {
Box::new(Orientation::Horizontal, 4);
..set_margin_start(4);
..set_margin_end(4);
..set_margin_bottom(4);
};
dock.add_css_class("dock");
revealer.set_child(Some(&dock));
let saved_app_list_view = DockList::new(DockListType::Saved);
dock.append(&saved_app_list_view);
let separator = cascade! {
Separator::new(Orientation::Vertical);
..set_margin_start(8);
..set_margin_end(8);
};
dock.append(&separator);
let active_app_list_view = DockList::new(DockListType::Active);
dock.append(&active_app_list_view);
imp.cursor_handle.set(cursor_handle).unwrap();
imp.revealer.set(revealer).unwrap();
imp.saved_list.set(saved_app_list_view).unwrap();
imp.active_list.set(active_app_list_view).unwrap();
// Setup
self_.setup_motion_controller();
self_.setup_drop_target();
self_.setup_callbacks();
self_
}
pub fn model(&self, type_: DockListType) -> &gio::ListStore {
// Get state
let imp = imp::Window::from_instance(self);
match type_ {
DockListType::Active => imp.active_list.get().unwrap().model(),
DockListType::Saved => imp.saved_list.get().unwrap().model(),
}
}
fn setup_callbacks(&self) {
// Get state
let imp = imp::Window::from_instance(self);
let window = self.clone().upcast::<gtk4::Window>();
let cursor_event_controller = &imp.cursor_motion_controller.get().unwrap();
// let drop_controller = &imp.drop_controller.get().unwrap();
let window_drop_controller = &imp.window_drop_controller.get().unwrap();
let revealer = &imp.revealer.get().unwrap();
window.connect_show(
glib::clone!(@weak revealer, @weak cursor_event_controller => move |_| {
// dbg!(!cursor_event_controller.contains_pointer());
if !cursor_event_controller.contains_pointer() {
revealer.set_reveal_child(false);
}
}),
);
window.connect_realize(glib::clone!(@weak revealer, @weak window_drop_controller, @weak cursor_event_controller => move |window| {
if let Some((display, surface)) = x::get_window_x11(window) {
// ignore all x11 errors...
let xdisplay = display.clone().downcast::<X11Display>().expect("Failed to downgrade X11 Display.");
xdisplay.error_trap_push();
unsafe {
x::change_property(
&display,
&surface,
"_NET_WM_WINDOW_TYPE",
x::PropMode::Replace,
&[x::Atom::new(&display, "_NET_WM_WINDOW_TYPE_DOCK").unwrap()],
);
}
let resize = glib::clone!(@weak window, @weak revealer => move || {
let height = if revealer.reveals_child() { window.height() } else { 4 };
let width = window.width();
if let Some((display, _surface)) = x::get_window_x11(&window) {
let geom = display
.primary_monitor().geometry();
let monitor_x = geom.x();
let monitor_y = geom.y();
let monitor_width = geom.width();
let monitor_height = geom.height();
// dbg!(monitor_x);
// dbg!(monitor_y);
// dbg!(monitor_width);
// dbg!(monitor_height);
// dbg!(width);
// dbg!(height);
unsafe { x::set_position(&display, &surface,
(monitor_x + monitor_width / 2 - width / 2).clamp(0, monitor_x + monitor_width - 1),
(monitor_y + monitor_height - height).clamp(0, monitor_y + monitor_height - 1));}
}
});
let resize_drop = resize.clone();
window_drop_controller.connect_enter(glib::clone!(@weak revealer, @weak window => @default-return gdk4::DragAction::COPY, move |_self, _x, _y| {
glib::source::idle_add_local_once(resize_drop.clone());
revealer.set_reveal_child(true);
gdk4::DragAction::COPY
}));
let resize_cursor = resize.clone();
cursor_event_controller.connect_enter(glib::clone!(@weak revealer, @weak window => move |_evc, _x, _y| {
// dbg!("hello, mouse entered me :)");
revealer.set_reveal_child(true);
glib::source::idle_add_local_once(resize_cursor.clone());
}));
let resize_revealed = resize.clone();
revealer.connect_child_revealed_notify(glib::clone!(@weak window => move |r| {
if !r.is_child_revealed() {
glib::source::idle_add_local_once(resize_revealed.clone());
}
}));
let s = window.surface();
let resize_height = resize.clone();
s.connect_height_notify(move |_s| {
glib::source::idle_add_local_once(resize_height.clone());
});
let resize_width = resize.clone();
s.connect_width_notify(move |_s| {
glib::source::idle_add_local_once(resize_width.clone());
});
s.connect_scale_factor_notify(move |_s| {
glib::source::idle_add_local_once(resize.clone());
});
} else {
println!("failed to get X11 window");
}
}));
let drop_controller = imp.saved_list.get().unwrap().drop_controller();
cursor_event_controller.connect_leave(
glib::clone!(@weak revealer, @weak drop_controller => move |_evc| {
// only hide if DnD is not happening
if drop_controller.current_drop().is_none() {
// dbg!("hello, mouse left me :)");
revealer.set_reveal_child(false);
}
}),
);
// hack to prevent hiding window when dnd from other apps
drop_controller.connect_enter(glib::clone!(@weak revealer => @default-return gdk4::DragAction::COPY, move |_self, _x, _y| {
revealer.set_reveal_child(true);
gdk4::DragAction::COPY
}));
window_drop_controller.connect_drop(|_, _, _, _| {
println!("dropping into window");
false
});
}
fn setup_motion_controller(&self) {
let imp = imp::Window::from_instance(self);
let handle = &imp.cursor_handle.get().unwrap();
let ev = EventControllerMotion::builder()
.propagation_limit(gtk4::PropagationLimit::None)
.propagation_phase(gtk4::PropagationPhase::Capture)
.build();
handle.add_controller(&ev);
imp.cursor_motion_controller
.set(ev)
.expect("Could not set event controller");
}
fn setup_drop_target(&self) {
// hack for revealing hidden dock when drag enters dock window
let imp = imp::Window::from_instance(self);
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));
let window_drop_target_controller = DropTarget::builder()
.actions(drop_actions)
.formats(&drop_format)
.build();
let enter_handle = &imp.cursor_handle.get().unwrap();
enter_handle.add_controller(&window_drop_target_controller);
imp.window_drop_controller
.set(window_drop_target_controller)
.expect("Could not set dock dnd drop controller");
}
}

View file

@ -1,128 +0,0 @@
use gdk4::Display;
use gio::DesktopAppInfo;
use gtk4::gio;
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::Application;
use gtk4::CssProvider;
use gtk4::StyleContext;
use once_cell::sync::OnceCell;
use pop_launcher_service::IpcClient;
use tokio::sync::mpsc;
use crate::utils::BoxedSearchResult;
use self::search_result_object::SearchResultObject;
use self::window::Window;
mod search_result_object;
mod search_result_row;
mod utils;
mod window;
const NUM_LAUNCHER_ITEMS: u8 = 10;
static TX: OnceCell<mpsc::Sender<Event>> = OnceCell::new();
pub enum Event {
Response(pop_launcher::Response),
Search(String),
Activate(u32),
}
fn spawn_launcher(tx: mpsc::Sender<Event>) -> IpcClient {
let (launcher, responses) =
pop_launcher_service::IpcClient::new().expect("failed to connect to launcher service");
glib::MainContext::default().spawn_local(async move {
use futures::StreamExt;
futures::pin_mut!(responses);
while let Some(event) = responses.next().await {
let _ = tx.send(Event::Response(event)).await;
}
});
launcher
}
fn setup_shortcuts(app: &Application) {
//quit shortcut
app.set_accels_for_action("win.quit", &["<primary>W", "Escape"]);
//launch shortcuts
for i in 1..NUM_LAUNCHER_ITEMS {
app.set_accels_for_action(&format!("win.launch{}", i), &[&format!("<primary>{}", i)]);
}
}
fn load_css() {
// Load the css file and add it to the provider
let provider = CssProvider::new();
provider.load_from_data(include_bytes!("style.css"));
// Add the provider to the default screen
StyleContext::add_provider_for_display(
&Display::default().expect("Error initializing GTK CSS provider."),
&provider,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn main() {
let app = gtk4::Application::builder()
.application_id("com.cosmic.Launcher")
.build();
app.connect_startup(|app| {
setup_shortcuts(app);
load_css()
});
app.connect_activate(move |app| {
let (tx, mut rx) = mpsc::channel(100);
let mut launcher = spawn_launcher(tx.clone());
if TX.set(tx).is_err() {
println!("failed to set global Sender. Exiting");
std::process::exit(1);
};
let window = Window::new(app);
window.show();
glib::MainContext::default().spawn_local(async move {
while let Some(event) = rx.recv().await {
match event {
Event::Search(search) => {
let _ = launcher.send(pop_launcher::Request::Search(search)).await;
}
Event::Activate(index) => {
let _ = launcher.send(pop_launcher::Request::Activate(index)).await;
}
Event::Response(event) => {
if let pop_launcher::Response::Update(results) = event {
let model = window.model();
let model_len = model.n_items();
dbg!(&results);
let new_results: Vec<glib::Object> = results
// [0..std::cmp::min(results.len(), NUM_LAUNCHER_ITEMS.into())]
.into_iter()
.map(|result| SearchResultObject::new(&BoxedSearchResult(Some(result))).upcast())
.collect();
model.splice(0, model_len, &new_results[..]);
} else if let pop_launcher::Response::DesktopEntry {
path,
gpu_preference: _gpu_preference, // TODO use GPU preference when launching app
} = event
{
let app_info =
DesktopAppInfo::new(&path.file_name().expect("desktop entry path needs to be a valid filename").to_string_lossy())
.expect("failed to create a Desktop App info for launching the application.");
app_info
.launch(&[], Some(&window.display().app_launch_context())).expect("failed to launch the application.");
}
}
}
}
});
});
app.run();
}

View file

@ -1,61 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use glib::{ParamFlags, ParamSpec, ParamSpecBoxed, Value};
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use once_cell::sync::Lazy;
use crate::utils::BoxedSearchResult;
// Object holding the state
#[derive(Default)]
pub struct SearchResultObject {
data: Rc<RefCell<BoxedSearchResult>>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for SearchResultObject {
const NAME: &'static str = "SearchResultObject";
type Type = super::SearchResultObject;
type ParentType = glib::Object;
}
// Trait shared by all GObjects
impl ObjectImpl for SearchResultObject {
fn properties() -> &'static [ParamSpec] {
static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| {
vec![ParamSpecBoxed::new(
// Name
"data",
// Nickname
"data",
// Short description
"data",
BoxedSearchResult::static_type(),
// The property can be read and written to
ParamFlags::READWRITE,
)]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _obj: &Self::Type, _id: usize, value: &Value, pspec: &ParamSpec) {
match pspec.name() {
"data" => {
let data = value.get().expect("Value needs to be BoxedSearchResult");
self.data.replace(data);
}
_ => unimplemented!(),
}
}
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> Value {
match pspec.name() {
"data" => self.data.borrow().to_value(),
_ => unimplemented!(),
}
}
}

View file

@ -1,20 +0,0 @@
use crate::utils::BoxedSearchResult;
use gtk4::glib;
use gtk4::prelude::*;
mod imp;
glib::wrapper! {
pub struct SearchResultObject(ObjectSubclass<imp::SearchResultObject>);
}
impl SearchResultObject {
pub fn new(search_result: &BoxedSearchResult) -> Self {
glib::Object::new(&[("data", search_result)]).expect("Failed to create Application Object")
}
pub fn data(&self) -> Option<pop_launcher::SearchResult> {
let search_result = self.property::<BoxedSearchResult>("data");
return search_result.0;
}
}

View file

@ -1,26 +0,0 @@
use gtk4::glib;
use gtk4::subclass::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Default)]
pub struct SearchResultRow {
pub name: Rc<RefCell<gtk4::Label>>,
pub description: Rc<RefCell<gtk4::Label>>,
pub shortcut: Rc<RefCell<gtk4::Label>>,
pub image: Rc<RefCell<gtk4::Image>>,
pub category_image: Rc<RefCell<gtk4::Image>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SearchResultRow {
const NAME: &'static str = "SearchResultRow";
type Type = super::SearchResultRow;
type ParentType = gtk4::Box;
}
impl ObjectImpl for SearchResultRow {}
impl WidgetImpl for SearchResultRow {}
impl BoxImpl for SearchResultRow {}

View file

@ -1,124 +0,0 @@
use cascade::cascade;
use gtk4::glib;
use gtk4::pango::EllipsizeMode;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::Align;
use gtk4::Box;
use gtk4::Image;
use gtk4::Label;
use gtk4::Orientation;
use crate::utils::icon_source;
use crate::BoxedSearchResult;
use crate::SearchResultObject;
mod imp;
glib::wrapper! {
pub struct SearchResultRow(ObjectSubclass<imp::SearchResultRow>)
@extends gtk4::Widget, gtk4::Box,
@implements gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Orientable;
}
impl Default for SearchResultRow {
fn default() -> Self {
Self::new()
}
}
impl SearchResultRow {
pub fn new() -> Self {
let self_ = glib::Object::new(&[]).expect("Failed to create SearchResultRow");
let imp = imp::SearchResultRow::from_instance(&self_);
cascade! {
&self_;
..set_orientation(Orientation::Horizontal);
..set_spacing(12);
..set_margin_start(4);
..set_margin_end(4);
..set_hexpand(true);
};
let category_image = cascade! {
Image::new();
..set_pixel_size(24);
};
self_.append(&category_image);
let image = cascade! {
Image::new();
..set_margin_top(4);
..set_margin_bottom(4);
..set_pixel_size(40);
};
self_.append(&image);
let text_container = cascade! {
Box::new(Orientation::Vertical, 0);
..set_halign(Align::Fill);
..set_hexpand(true);
..set_margin_top(4);
..set_margin_end(4);
..set_margin_bottom(4);
};
self_.append(&text_container);
let shortcut = cascade! {
Label::new(None);
..set_halign(Align::End);
..set_wrap(false);
..add_css_class("body");
};
self_.append(&shortcut);
let name = cascade! {
Label::new(None);
..set_halign(Align::Start);
..set_ellipsize(EllipsizeMode::End);
..set_max_width_chars(40);
..add_css_class("title-4");
};
text_container.append(&name);
let description = cascade! {
Label::new(None);
..set_halign(Align::Start);
..set_ellipsize(EllipsizeMode::End);
..set_max_width_chars(50);
..add_css_class("body");
};
text_container.append(&description);
imp.category_image.replace(category_image);
imp.image.replace(image);
imp.name.replace(name);
imp.description.replace(description);
imp.shortcut.replace(shortcut);
self_
}
pub fn set_search_result(&self, search_obj: SearchResultObject) {
let self_ = imp::SearchResultRow::from_instance(self);
let search_result = search_obj.property::<BoxedSearchResult>("data");
if let Some(search_result) = search_result.0 {
self_.name.borrow().set_text(&search_result.name);
self_
.description
.borrow()
.set_text(&search_result.description);
icon_source(&self_.image, &search_result.icon);
icon_source(&self_.category_image, &search_result.category_icon);
}
}
pub fn set_shortcut(&self, indx: u32) {
let self_ = imp::SearchResultRow::from_instance(self);
self_
.shortcut
.borrow()
.set_text(&format!("Ctrl + {}", indx));
}
}

View file

@ -1,25 +0,0 @@
listview row:selected {
transition: 100ms;
background: #888888;
border-radius: 8px;
}
listview row {
transition: 100ms;
background: #333333;
border-radius: 8px;
}
listview {
background: #333333;
}
box.container {
background: #333333;
padding: 12px;
border-radius: 12px;
}
window.root_window {
background: rgba(50, 50, 50, 0.0);
}

View file

@ -1,22 +0,0 @@
use gtk4::glib;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Clone, Debug, Default, glib::Boxed)]
#[boxed_type(name = "BoxedSearchResult")]
pub struct BoxedSearchResult(pub Option<pop_launcher::SearchResult>);
pub fn icon_source(icon: &Rc<RefCell<gtk4::Image>>, source: &Option<pop_launcher::IconSource>) {
match source {
Some(pop_launcher::IconSource::Name(name)) => {
icon.borrow().set_from_icon_name(Some(name));
}
Some(pop_launcher::IconSource::Mime(content_type)) => {
icon.borrow()
.set_from_gicon(&gio::content_type_get_icon(content_type));
}
_ => {
icon.borrow().set_from_icon_name(None);
}
}
}

View file

@ -1,33 +0,0 @@
use gtk4::subclass::prelude::*;
use gtk4::{gio, glib};
use gtk4::{Entry, ListView};
use once_cell::sync::OnceCell;
// Object holding the state
#[derive(Default)]
pub struct Window {
pub entry: OnceCell<Entry>,
pub list_view: OnceCell<ListView>,
pub model: OnceCell<gio::ListStore>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "LauncherWindow";
type Type = super::Window;
type ParentType = gtk4::ApplicationWindow;
}
// Trait shared by all GObjects
impl ObjectImpl for Window {}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {}
// Trait shared by all application
impl ApplicationWindowImpl for Window {}

View file

@ -1,271 +0,0 @@
use cascade::cascade;
use gdk4_x11::X11Display;
use glib::Object;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use gtk4::Box;
use gtk4::Entry;
use gtk4::ListView;
use gtk4::Orientation;
use gtk4::{gio, glib};
use gtk4::{Application, SignalListItemFactory};
use libcosmic::x;
use crate::search_result_row::SearchResultRow;
use crate::SearchResultObject;
use crate::TX;
mod imp;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk4::ApplicationWindow, gtk4::Window, gtk4::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk4::Accessible, gtk4::Buildable,
gtk4::ConstraintTarget, gtk4::Native, gtk4::Root, gtk4::ShortcutManager;
}
const NUM_LAUNCHER_ITEMS: u8 = 9;
impl Window {
pub fn new(app: &Application) -> Self {
let self_: Self = Object::new(&[("application", app)]).expect("Failed to create `Window`.");
let imp = imp::Window::from_instance(&self_);
cascade! {
&self_;
..set_width_request(600);
..set_title(Some("Cosmic Launcher"));
..set_decorated(false);
..set_resizable(false);
..add_css_class("root_window");
};
let container = cascade! {
Box::new(Orientation::Vertical, 0);
..add_css_class("container");
};
self_.set_child(Some(&container));
let entry = cascade! {
Entry::new();
..set_margin_bottom(12);
};
container.append(&entry);
let list_view = cascade! {
ListView::default();
..set_orientation(Orientation::Vertical);
..set_single_click_activate(true);
};
container.append(&list_view);
imp.entry.set(entry).unwrap();
imp.list_view.set(list_view).unwrap();
// Setup
self_.setup_model();
self_.setup_callbacks();
self_.setup_factory();
self_
}
pub fn model(&self) -> &gio::ListStore {
// Get state
let imp = imp::Window::from_instance(self);
imp.model.get().expect("Could not get model")
}
fn setup_model(&self) {
// Get state and set model
let imp = imp::Window::from_instance(self);
let model = gio::ListStore::new(SearchResultObject::static_type());
let slice_model = gtk4::SliceListModel::new(Some(&model), 0, NUM_LAUNCHER_ITEMS.into());
let selection_model = gtk4::SingleSelection::builder()
.model(&slice_model)
.autoselect(false)
.can_unselect(true)
.selected(gtk4::INVALID_LIST_POSITION)
.build();
imp.model.set(model).expect("Could not set model");
// Wrap model with selection and pass it to the list view
imp.list_view
.get()
.unwrap()
.set_model(Some(&selection_model));
}
fn setup_callbacks(&self) {
// Get state
let imp = imp::Window::from_instance(self);
let window = self.clone().upcast::<gtk4::Window>();
let list_view = &imp.list_view;
let entry = &imp.entry.get().unwrap();
let lv = list_view.get().unwrap();
for i in 1..10 {
let action_launchi = gio::SimpleAction::new(&format!("launch{}", i), None);
self.add_action(&action_launchi);
action_launchi.connect_activate(glib::clone!(@weak lv => move |_action, _parameter| {
let i = i - 1;
println!("activating... {}", i);
let model = lv.model().unwrap();
let obj = match model.item(i) {
Some(obj) => obj.downcast::<SearchResultObject>().unwrap(),
None => {
dbg!(model.item(i));
return;
},
};
if let Some(search_result) = obj.data() {
println!("activating... {}", i + 1);
glib::MainContext::default().spawn_local(async move {
if let Some(tx) = TX.get() {
let _ = tx.send(crate::Event::Activate(search_result.id)).await;
}
});
}
}));
}
lv.connect_activate(glib::clone!(@weak window => move |list_view, i| {
dbg!(i);
let model = list_view.model()
.expect("List view missing selection model")
.downcast::<gtk4::SingleSelection>()
.expect("could not downcast listview model to no selection model");
if i >= model.n_items() {
dbg!("index out of range");
return;
}
let obj = match model.item(i) {
Some(obj) => obj.downcast::<SearchResultObject>().unwrap(),
None => {
dbg!(model.item(i));
return;
},
};
if let Some(search_result) = obj.data() {
println!("activating... {}", i + 1);
glib::MainContext::default().spawn_local(async move {
if let Some(tx) = TX.get() {
let _ = tx.send(crate::Event::Activate(search_result.id)).await;
}
});
}
}));
entry.connect_changed(glib::clone!(@weak lv => move |search: &gtk4::Entry| {
let search = search.text().to_string();
glib::MainContext::default().spawn_local(async move {
if let Some(tx) = TX.get() {
let _ = tx.send(crate::Event::Search(search)).await;
}
});
}));
entry.connect_realize(glib::clone!(@weak lv => move |search: &gtk4::Entry| {
let search = search.text().to_string();
glib::MainContext::default().spawn_local(async move {
if let Some(tx) = TX.get() {
let _ = tx.send(crate::Event::Search(search)).await;
}
});
}));
window.connect_realize(move |window| {
if let Some((display, surface)) = x::get_window_x11(window) {
// ignore all x11 errors...
let xdisplay = display.clone().downcast::<X11Display>().expect("Failed to downgrade X11 Display.");
xdisplay.error_trap_push();
unsafe {
x::change_property(
&display,
&surface,
"_NET_WM_WINDOW_TYPE",
x::PropMode::Replace,
&[x::Atom::new(&display, "_NET_WM_WINDOW_TYPE_DIALOG").unwrap()],
);
}
let resize = glib::clone!(@weak window => move || {
let height = window.height();
let width = window.width();
if let Some((display, _surface)) = x::get_window_x11(&window) {
let geom = display
.primary_monitor().geometry();
let monitor_x = geom.x();
let monitor_y = geom.y();
let monitor_width = geom.width();
let monitor_height = geom.height();
// dbg!(monitor_width);
// dbg!(monitor_height);
// dbg!(width);
// dbg!(height);
unsafe { x::set_position(&display, &surface,
(monitor_x + monitor_width / 2 - width / 2).clamp(0, monitor_x + monitor_width - 1),
(monitor_y + monitor_height / 2 - height / 2).clamp(0, monitor_y + monitor_height - 1))};
}
});
let s = window.surface();
let resize_height = resize.clone();
s.connect_height_notify(move |_s| {
glib::source::idle_add_local_once(resize_height.clone());
});
let resize_width = resize.clone();
s.connect_width_notify(move |_s| {
glib::source::idle_add_local_once(resize_width.clone());
});
s.connect_scale_factor_notify(move |_s| {
glib::source::idle_add_local_once(resize.clone());
});
} else {
println!("failed to get X11 window");
}
});
let action_quit = gio::SimpleAction::new("quit", None);
action_quit.connect_activate(glib::clone!(@weak window => move |_, _| {
window.close();
}));
self.add_action(&action_quit);
window.connect_is_active_notify(|win| {
if !win.is_active() {
win.close();
}
});
}
fn setup_factory(&self) {
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let row = SearchResultRow::new();
list_item.set_child(Some(&row))
});
factory.connect_bind(move |_, list_item| {
let application_object = list_item
.item()
.expect("The item has to exist.")
.downcast::<SearchResultObject>()
.expect("The item has to be an `SearchResultObject`");
let row = list_item
.child()
.expect("The list item child needs to exist.")
.downcast::<SearchResultRow>()
.expect("The list item type needs to be `SearchResultRow`");
if list_item.position() < 9 {
row.set_shortcut(list_item.position() + 1);
}
row.set_search_result(application_object);
});
// Set the factory of the list view
let imp = imp::Window::from_instance(self);
imp.list_view.get().unwrap().set_factory(Some(&factory));
}
}