Skip to content

Commit

Permalink
Windows taskbar progress support (qarmin#264)
Browse files Browse the repository at this point in the history
* Initial Windows taskbar progress support

* Changes to COM (un)init

It turns out winapi exposes IIDs through a `uuidof()` function of interfaces, so the copied one can be removed.

* Don't return error codes

Now the `TaskbarProgress` functions fail silently.
The `TaskbarProgress` struct now will always be created (even in case of errors in initialisation), but it won't do anything.

* Fix builds for other systems

* Formatted code

* Fix progress shown after the operation finished

A progress update was received after the stop event.
Also `as_ref()` was removed in many places (I don't even know why it was there).

* Remove redundant call to hide

It's already called by the `glib_stop_receiver` receiver.

* Release the ITaskbarList3 and call CoUninitialize at exit

Because objects moved to closures used as fallbacks in GTK have [static lifetimes](https://gtk-rs.org/docs-src/tutorial/closures#closures), the `TaskbarProgress` will never be dropped.
To workaround this problem a `release` function is called when the main window is closed. This function behaves like `drop`, but sets the struct in a valid "empty" state, so that calling `release`/`drop` again won't cause problems.

* Don't set the NORMAL state manually

Because only NOPROGRESS and INDETERMINATE states are used, there is no need to set the NORMAL state when changing the progress value.

Now `set_progress_value` will also change the `TaskbarProgress::current_state` if such situation occurs.

> Unless [SetProgressState](https://docs.microsoft.com/en-us/windows/desktop/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate)
> has set a blocking state (TBPF_ERROR or TBPF_PAUSED) for the window, a call to **SetProgressValue** assumes the TBPF_NORMAL
> state even if it is not explicitly set. A call to **SetProgressValue** overrides and clears the TBPF_INDETERMINATE state.

See the [SetProgressValue documentation](https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressvalue#how-the-taskbar-button-chooses-the-progress-indicator-for-a-group)
  • Loading branch information
krzysdz authored and LJason77 committed Feb 20, 2021
1 parent 3add28a commit df7a4a3
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions czkawka_gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ open = "1.4.0"
# To get image preview
image = "0.23.12"

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["combaseapi", "objbase", "shobjidl_core", "windef", "winerror", "wtypesbase", "winuser"] }

[dependencies.gtk]
version = "0.9.2"
default-features = false # just in case
Expand Down
5 changes: 5 additions & 0 deletions czkawka_gui/src/connect_button_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;

use crate::taskbar_progress::tbp_flags::TBPF_NOPROGRESS;

#[allow(clippy::too_many_arguments)]
pub fn connect_button_search(
gui_data: &GuiData,
Expand Down Expand Up @@ -83,6 +85,7 @@ pub fn connect_button_search(
let grid_progress_stages = gui_data.progress_window.grid_progress_stages.clone();
let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone();
let progress_bar_all_stages = gui_data.progress_window.progress_bar_all_stages.clone();
let taskbar_state = gui_data.taskbar_state.clone();
let image_preview_similar_images = gui_data.main_notebook.image_preview_similar_images.clone();
let radio_button_hash_type_blake3 = gui_data.main_notebook.radio_button_hash_type_blake3.clone();
let radio_button_hash_type_crc32 = gui_data.main_notebook.radio_button_hash_type_crc32.clone();
Expand Down Expand Up @@ -405,6 +408,8 @@ pub fn connect_button_search(
// Show progress dialog
if show_dialog.load(Ordering::Relaxed) {
window_progress.show();
taskbar_state.borrow().show();
taskbar_state.borrow().set_progress_state(TBPF_NOPROGRESS);
}
});
}
3 changes: 3 additions & 0 deletions czkawka_gui/src/connect_compute_results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,15 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver<
let shared_same_music_state = gui_data.shared_same_music_state.clone();
let buttons_names = gui_data.bottom_buttons.buttons_names.clone();
let window_progress = gui_data.progress_window.window_progress.clone();
let taskbar_state = gui_data.taskbar_state.clone();

glib_stop_receiver.attach(None, move |msg| {
buttons_search.show();

window_progress.hide();

taskbar_state.borrow().hide();

// Restore clickability to main notebook
notebook_main.set_sensitive(true);

Expand Down
43 changes: 43 additions & 0 deletions czkawka_gui/src/connect_progress_window.rs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions czkawka_gui/src/gui_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::gui_popovers::GUIPopovers;
use crate::gui_progress_dialog::GUIProgressDialog;
use crate::gui_upper_notepad::GUIUpperNotebook;
use crate::notebook_enums::*;
use crate::taskbar_progress::TaskbarProgress;
use crossbeam_channel::unbounded;
use czkawka_core::big_file::BigFile;
use czkawka_core::broken_files::BrokenFiles;
Expand Down Expand Up @@ -43,6 +44,9 @@ pub struct GuiData {
pub options: GUIOptions,
pub header: GUIHeader,

// Taskbar state
pub taskbar_state: Rc<RefCell<TaskbarProgress>>,

// Buttons state
pub shared_buttons: Rc<RefCell<HashMap<NotebookMainEnum, HashMap<String, bool>>>>,

Expand Down Expand Up @@ -95,6 +99,9 @@ impl GuiData {

////////////////////////////////////////////////////////////////////////////////////////////////

// Taskbar state
let taskbar_state = Rc::new(RefCell::new(TaskbarProgress::new()));

// Buttons State - to remember existence of different buttons on pages
let shared_buttons: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::<NotebookMainEnum, HashMap<String, bool>>::new()));

Expand Down Expand Up @@ -160,6 +167,7 @@ impl GuiData {
about,
options,
header,
taskbar_state,
shared_buttons,
shared_upper_notebooks,
shared_duplication_state,
Expand Down
7 changes: 7 additions & 0 deletions czkawka_gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ mod help_functions;
mod initialize_gui;
mod notebook_enums;
mod saving_loading;
mod taskbar_progress;
#[cfg(not(target_os = "windows"))]
mod taskbar_progress_dummy;
#[cfg(target_os = "windows")]
mod taskbar_progress_win;

use czkawka_core::*;

Expand Down Expand Up @@ -140,9 +145,11 @@ fn main() {
// Quit the program when X in main window was clicked
{
let window_main = gui_data.window_main.clone();
let taskbar_state = gui_data.taskbar_state.clone();
window_main.connect_delete_event(move |_, _| {
save_configuration(&gui_data, false); // Save configuration at exit
gtk::main_quit();
taskbar_state.borrow_mut().release();
Inhibit(false)
});
}
Expand Down
5 changes: 5 additions & 0 deletions czkawka_gui/src/taskbar_progress.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[cfg(target_os = "windows")]
pub use crate::taskbar_progress_win::{tbp_flags, TaskbarProgress};

#[cfg(not(target_os = "windows"))]
pub use crate::taskbar_progress_dummy::{tbp_flags, TaskbarProgress};
46 changes: 46 additions & 0 deletions czkawka_gui/src/taskbar_progress_dummy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#![cfg(not(target_os = "windows"))]
use std::convert::From;

enum HWND__ {}
type HWND = *mut HWND__;

#[allow(non_camel_case_types, dead_code)]
pub enum TBPFLAG {
TBPF_NOPROGRESS = 0,
TBPF_INDETERMINATE = 0x1,
TBPF_NORMAL = 0x2,
TBPF_ERROR = 0x4,
TBPF_PAUSED = 0x8,
}

pub mod tbp_flags {
pub use super::TBPFLAG::*;
}

pub struct TaskbarProgress {}

impl TaskbarProgress {
pub fn new() -> TaskbarProgress {
TaskbarProgress {}
}

pub fn set_progress_state(&self, _tbp_flags: TBPFLAG) {}

pub fn set_progress_value(&self, _completed: u64, _total: u64) {}

pub fn hide(&self) {}

pub fn show(&self) {}

pub fn release(&mut self) {}
}

impl From<HWND> for TaskbarProgress {
fn from(_hwnd: HWND) -> Self {
TaskbarProgress {}
}
}

impl Drop for TaskbarProgress {
fn drop(&mut self) {}
}
154 changes: 154 additions & 0 deletions czkawka_gui/src/taskbar_progress_win.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#![cfg(target_os = "windows")]
extern crate winapi;
use std::cell::RefCell;
use std::convert::From;
use std::ptr;
use winapi::ctypes::c_void;
use winapi::shared::windef::HWND;
use winapi::shared::winerror::{E_POINTER, S_OK};
use winapi::shared::wtypesbase::CLSCTX_INPROC_SERVER;
use winapi::um::shobjidl_core::{CLSID_TaskbarList, ITaskbarList3, TBPFLAG};
use winapi::um::{combaseapi, objbase, winuser};
use winapi::Interface;

pub mod tbp_flags {
pub use winapi::um::shobjidl_core::{TBPF_ERROR, TBPF_INDETERMINATE, TBPF_NOPROGRESS, TBPF_NORMAL, TBPF_PAUSED};
}

pub struct TaskbarProgress {
hwnd: HWND,
taskbar_list: *mut ITaskbarList3,
current_state: RefCell<TBPFLAG>,
current_progress: RefCell<(u64, u64)>,
must_uninit_com: bool,
is_active: RefCell<bool>,
}

impl TaskbarProgress {
pub fn new() -> TaskbarProgress {
let hwnd = unsafe { winuser::GetActiveWindow() };
TaskbarProgress::from(hwnd)
}

pub fn set_progress_state(&self, tbp_flags: TBPFLAG) {
if tbp_flags == *self.current_state.borrow() || !*self.is_active.borrow() {
return ();
}
let result = unsafe {
if let Some(list) = self.taskbar_list.as_ref() {
list.SetProgressState(self.hwnd, tbp_flags)
} else {
E_POINTER
}
};
if result == S_OK {
self.current_state.replace(tbp_flags);
}
}

pub fn set_progress_value(&self, completed: u64, total: u64) {
// Don't change the value if the is_active flag is false or the value has not changed.
// If is_active is true and the value has not changed, but the progress indicator was in NOPROGRESS or INDETERMINATE state, set the value (and NORMAL state).
if ((completed, total) == *self.current_progress.borrow() && *self.current_state.borrow() != tbp_flags::TBPF_NOPROGRESS && *self.current_state.borrow() != tbp_flags::TBPF_INDETERMINATE) || !*self.is_active.borrow() {
return ();
}
let result = unsafe {
if let Some(list) = self.taskbar_list.as_ref() {
list.SetProgressValue(self.hwnd, completed, total)
} else {
E_POINTER
}
};
if result == S_OK {
self.current_progress.replace((completed, total));
if *self.current_state.borrow() == tbp_flags::TBPF_NOPROGRESS || *self.current_state.borrow() == tbp_flags::TBPF_INDETERMINATE {
self.current_state.replace(tbp_flags::TBPF_NORMAL);
}
}
}

pub fn hide(&self) {
self.set_progress_state(tbp_flags::TBPF_NOPROGRESS);
*self.is_active.borrow_mut() = false;
}

pub fn show(&self) {
*self.is_active.borrow_mut() = true;
}

/// Releases the ITaskbarList3 pointer, uninitialises the COM API and sets the struct to a valid "empty" state.
/// It's required for proper use of the COM API, because `drop` is never called (objects moved to GTK closures have `static` lifetime).
pub fn release(&mut self) {
unsafe {
if let Some(list) = self.taskbar_list.as_ref() {
list.Release();
self.taskbar_list = ptr::null_mut();
self.hwnd = ptr::null_mut();
}
// A thread must call CoUninitialize once for each successful call it has made to
// the CoInitialize or CoInitializeEx function, including any call that returns S_FALSE.
if self.must_uninit_com {
combaseapi::CoUninitialize();
self.must_uninit_com = false;
}
}
}
}

impl From<HWND> for TaskbarProgress {
fn from(hwnd: HWND) -> Self {
if hwnd.is_null() {
return TaskbarProgress {
hwnd,
taskbar_list: ptr::null_mut(),
current_state: RefCell::new(tbp_flags::TBPF_NOPROGRESS),
current_progress: RefCell::new((0, 0)),
must_uninit_com: false,
is_active: RefCell::new(false),
};
}

let init_result = unsafe { combaseapi::CoInitializeEx(ptr::null_mut(), objbase::COINIT_APARTMENTTHREADED) };
// S_FALSE means that COM library is already initialised for this thread
// Success codes are not negative, RPC_E_CHANGED_MODE should not be possible and is treated as an error
if init_result < 0 {
return TaskbarProgress {
hwnd: ptr::null_mut(),
taskbar_list: ptr::null_mut(),
current_state: RefCell::new(tbp_flags::TBPF_NOPROGRESS),
current_progress: RefCell::new((0, 0)),
must_uninit_com: false,
is_active: RefCell::new(false),
};
}

let mut taskbar_list: *mut ITaskbarList3 = ptr::null_mut();
let taskbar_list_ptr: *mut *mut ITaskbarList3 = &mut taskbar_list;

unsafe { combaseapi::CoCreateInstance(&CLSID_TaskbarList, ptr::null_mut(), CLSCTX_INPROC_SERVER, &ITaskbarList3::uuidof(), taskbar_list_ptr as *mut *mut c_void) };

TaskbarProgress {
hwnd: if taskbar_list.is_null() { ptr::null_mut() } else { hwnd },
taskbar_list,
current_state: RefCell::new(tbp_flags::TBPF_NOPROGRESS), // Assume no progress
current_progress: RefCell::new((0, 0)),
must_uninit_com: true,
is_active: RefCell::new(false),
}
}
}

impl Drop for TaskbarProgress {
fn drop(&mut self) {
unsafe {
if let Some(list) = self.taskbar_list.as_ref() {
list.Release();
}
// A thread must call CoUninitialize once for each successful call it has made to
// the CoInitialize or CoInitializeEx function, including any call that returns S_FALSE.
if self.must_uninit_com {
combaseapi::CoUninitialize();
}
}
}
}

0 comments on commit df7a4a3

Please sign in to comment.