Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/core/clipboard/clipboard-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,33 @@ export class VicinaeClipboardManager {
private selection: Meta.Selection | null = null;
private signals = new SignalRegistry();
private settings: Gio.Settings | null = null;
private isMonitoring = false;

constructor() {
this.contentHandlers = createHandlers();
this.clipboard = St.Clipboard.get_default();
this.selection = Shell.Global.get().get_display().get_selection();

if (!this.selection) {
logger.error(
"Failed to get selection instance in clipboard manager",
);
}
}

enable() {
this.setupClipboardMonitoring();
if (!this.isMonitoring) {
this.isMonitoring = true;
this.setupClipboardMonitoring();
}
}

disable() {
if (this.isMonitoring) {
this.signals.disconnectAll();
this.isMonitoring = false;
logger.info("Clipboard monitoring disabled");
}
}

setSettings(settings: Gio.Settings): void {
Expand Down Expand Up @@ -116,9 +136,6 @@ export class VicinaeClipboardManager {

private setupClipboardMonitoring() {
try {
this.clipboard = St.Clipboard.get_default();
this.selection = Shell.Global.get().get_display().get_selection();

if (this.selection) {
const selectionId = this.selection.connect(
"owner-changed",
Expand All @@ -133,8 +150,6 @@ export class VicinaeClipboardManager {
logger.info(
"Clipboard monitoring set up successfully using selection listener",
);
} else {
logger.error("Failed to get selection instance");
}
} catch (error) {
logger.error("Error setting up clipboard monitoring", error);
Expand Down Expand Up @@ -382,7 +397,7 @@ export class VicinaeClipboardManager {
}

destroy(): void {
this.signals.disconnectAll();
this.disable();
this.eventListeners = [];
this.currentContent = "";
logger.debug(
Expand Down
20 changes: 19 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export default class Vicinae extends Extension {
initializeLogger(this.settings);

this.clipboardManager = new VicinaeClipboardManager();
this.clipboardManager.enable();
if (this.settings.get_boolean("enable-clipboard-monitoring")) {
this.clipboardManager.enable();
}
this.clipboardManager.setSettings(this.settings);

const appClass =
Expand Down Expand Up @@ -93,6 +95,22 @@ export default class Vicinae extends Extension {
);
this.signals.add(() => this.settings?.disconnect(blockedAppsId));

const enableMonitoringId = this.settings.connect(
"changed::enable-clipboard-monitoring",
() => {
if (this.clipboardManager && this.settings) {
if (
this.settings.get_boolean("enable-clipboard-monitoring")
) {
this.clipboardManager.enable();
} else {
this.clipboardManager.disable();
}
}
},
);
this.signals.add(() => this.settings?.disconnect(enableMonitoringId));

logger.info("Vicinae extension initialized successfully");
}

Expand Down
5 changes: 5 additions & 0 deletions src/prefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type Gio from "gi://Gio";
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";

import { AboutPage } from "./prefs/AboutPage.js";
import { ClipboardPage } from "./prefs/ClipboardPage.js";
import { GeneralPage } from "./prefs/GeneralPage.js";

export default class VicinaePrefs extends ExtensionPreferences {
Expand All @@ -22,6 +23,10 @@ export default class VicinaePrefs extends ExtensionPreferences {
generalPage.bindSettings(prefsWindow._settings);
prefsWindow.add(generalPage);

const clipboardPage = new ClipboardPage();
clipboardPage.bindSettings(prefsWindow._settings);
prefsWindow.add(clipboardPage);

const aboutPage = new AboutPage();
aboutPage.setMetadata(this.metadata);
prefsWindow.add(aboutPage);
Expand Down
278 changes: 278 additions & 0 deletions src/prefs/ClipboardPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import Adw from "gi://Adw";
import Gio from "gi://Gio";
import GObject from "gi://GObject";
import type { ClipboardPageChildren } from "../types/prefs.js";
import { getTemplate } from "../utils/getTemplate.js";
import { logger } from "../utils/logger.js";
import {
BlockedAppRow,
type BlockedAppRowInstance,
} from "./components/blocked-app-row.js";

export const ClipboardPage = GObject.registerClass(
{
GTypeName: "VicinaeClipboardPage",
Template: getTemplate("ClipboardPage"),
InternalChildren: [
"enableClipboardMonitoring",
"blockedAppsGroup",
"emptyPlaceholderRow",
"addWindowButton",
],
},
class ClipboardPage extends Adw.PreferencesPage {
private settings!: Gio.Settings;
private rowInstances: Set<BlockedAppRowInstance> = new Set();

bindSettings(settings: Gio.Settings) {
this.settings = settings;
logger.debug("Settings bound to ClipboardPage");

this.loadBlockedApplications();
this.updateAddButtonState();

const children = this as unknown as ClipboardPageChildren;

this.connectAddBlockedAppButton(children);
this.bindEnableClipboardMonitoring(settings, children);
}

/** "Add window class" → append an empty blocked-app row. */
private connectAddBlockedAppButton(children: ClipboardPageChildren) {
children._addWindowButton.connect("clicked", () => {
this.addEmptyBlockedAppRow();
});
}

/** `enable-clipboard-monitoring` ↔ clipboard monitoring switch. */
private bindEnableClipboardMonitoring(
settings: Gio.Settings,
children: ClipboardPageChildren,
) {
settings.bind(
"enable-clipboard-monitoring",
children._enableClipboardMonitoring,
"active",
Gio.SettingsBindFlags.DEFAULT,
);

// Update sensitivity of blocked apps group based on the switch
const updateSensitivity = () => {
const isEnabled = settings.get_boolean(
"enable-clipboard-monitoring",
);
children._blockedAppsGroup.set_sensitive(isEnabled);
};

settings.connect(
"changed::enable-clipboard-monitoring",
updateSensitivity,
);
updateSensitivity();
}

private loadBlockedApplications() {
try {
const blockedApps = this.settings.get_strv(
"blocked-applications",
);

const uniqueBlockedApps = this.removeDuplicates(blockedApps);
if (uniqueBlockedApps.length !== blockedApps.length) {
this.settings.set_strv(
"blocked-applications",
uniqueBlockedApps,
);
}

const children = this as unknown as ClipboardPageChildren;
this.rowInstances.forEach((row) => {
children._blockedAppsGroup.remove(row);
});
this.rowInstances.clear();

uniqueBlockedApps.forEach((windowClass) => {
this.addBlockedAppRow(windowClass);
});
} catch (error) {
logger.error("Error loading blocked applications", error);
}
}

private removeDuplicates(apps: string[]): string[] {
const seen = new Set<string>();
return apps.filter((app) => {
const lowerApp = app.toLowerCase();
if (seen.has(lowerApp)) {
return false;
}
seen.add(lowerApp);
return true;
});
}

private addEmptyBlockedAppRow() {
const firstEmptyRow = Array.from(this.rowInstances).find((r) =>
r.isEmpty(),
);
if (firstEmptyRow) {
firstEmptyRow.focusInput();
return;
}
this.addBlockedAppRow("");
}

private addBlockedAppRow(windowClass: string) {
const children = this as unknown as ClipboardPageChildren;

const row = new BlockedAppRow();
row.setWindowClass(windowClass);

row.connect("delete-requested", () => {
this.removeBlockedAppRow(row);
});

row.connect("save-requested", () => {
this.handleSaveRequest(row);
});

row.connect("input-changed", () => {
this.handleInputChange(row);
});

children._blockedAppsGroup.add_row(row);

if (!windowClass) {
row.focusInput();
}

this.rowInstances.add(row);

this.updateAddButtonState();
}

private handleInputChange(_row: BlockedAppRowInstance) {
this.updateAddButtonState();
}

private updateAddButtonState() {
const children = this as unknown as ClipboardPageChildren;
const hasEmptyRows = Array.from(this.rowInstances).some((r) =>
r.isEmpty(),
);
children._addWindowButton.set_sensitive(!hasEmptyRows);

const hasAnyRows = this.rowInstances.size > 0;
children._emptyPlaceholderRow.set_visible(!hasAnyRows);
}

private handleSaveRequest(row: BlockedAppRowInstance) {
const oldClass = row.getOriginalWindowClass();
const newClass = row.getInputValue().trim();

if (newClass) {
const currentBlockedApps = this.settings.get_strv(
"blocked-applications",
);

const isDuplicate = currentBlockedApps.some(
(app) =>
app !== oldClass &&
app.toLowerCase() === newClass.toLowerCase(),
);

if (isDuplicate) {
const root = this.get_root() as Adw.PreferencesWindow;
if (root && "add_toast" in root) {
const toast = new Adw.Toast({
title: `Can't add ${newClass} to the list, because it's already there`,
});
(
root as Adw.PreferencesWindow & {
add_toast: (toast: Adw.Toast) => void;
}
).add_toast(toast);
}
row.setWindowClass(oldClass);
return;
}
this.updateBlockedAppInSettings(row, oldClass, newClass);
} else {
this.removeBlockedAppFromSettings(row, oldClass);
}
this.updateAddButtonState();
}

private updateBlockedAppInSettings(
_row: BlockedAppRowInstance,
oldClass: string,
newClass: string,
) {
try {
const currentBlockedApps = this.settings.get_strv(
"blocked-applications",
);

let filteredApps: string[];

if (oldClass && oldClass.trim() !== "") {
filteredApps = currentBlockedApps.filter(
(app) => app !== oldClass,
);
} else {
filteredApps = [...currentBlockedApps];
}

filteredApps.push(newClass);

this.settings.set_strv("blocked-applications", filteredApps);
} catch (error) {
logger.error("Error updating blocked app in settings", error);
}
}

private removeBlockedAppFromSettings(
_row: BlockedAppRowInstance,
oldClass: string,
) {
try {
if (!oldClass || oldClass.trim() === "") {
return;
}

const currentBlockedApps = this.settings.get_strv(
"blocked-applications",
);

const filteredApps = currentBlockedApps.filter(
(app) => app !== oldClass,
);

this.settings.set_strv("blocked-applications", filteredApps);
} catch (error) {
logger.error("Error removing blocked app from settings", error);
}
}

private removeBlockedAppRow(row: BlockedAppRowInstance) {
const children = this as unknown as ClipboardPageChildren;
const windowClass = row.getWindowClass();

if (windowClass) {
const currentApps = this.settings.get_strv(
"blocked-applications",
);

const updatedApps = currentApps.filter(
(app) => app !== windowClass,
);
this.settings.set_strv("blocked-applications", updatedApps);
}

this.rowInstances.delete(row);
children._blockedAppsGroup.remove(row);

this.updateAddButtonState();
}
},
);
Loading