v1
This commit is contained in:
285
extension/backend.js
Normal file
285
extension/backend.js
Normal file
@@ -0,0 +1,285 @@
|
||||
// backend.js - Interface sysfs et logique métier pour le rétroéclairage ASUS RGB
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
// Chemins sysfs pour le rétroéclairage ASUS
|
||||
const SYSFS_BASE = '/sys/class/leds/asus::kbd_backlight';
|
||||
const BRIGHTNESS_PATH = `${SYSFS_BASE}/brightness`;
|
||||
const MAX_BRIGHTNESS_PATH = `${SYSFS_BASE}/max_brightness`;
|
||||
const RGB_MODE_PATH = `${SYSFS_BASE}/kbd_rgb_mode`;
|
||||
|
||||
// Niveaux d'intensité (0=Off, 1=Faible, 2=Moyen, 3=Fort)
|
||||
const BRIGHTNESS_LEVELS = [0, null, null, null]; // Sera rempli dynamiquement avec max_brightness
|
||||
|
||||
// Timer pour le debouncing
|
||||
let debounceTimer = null;
|
||||
const DEBOUNCE_DELAY = 75; // ms
|
||||
|
||||
/**
|
||||
* Vérifie si le matériel ASUS RGB est présent sur le système
|
||||
* @returns {boolean} true si le matériel est supporté
|
||||
*/
|
||||
export function checkHardwareSupport() {
|
||||
try {
|
||||
const brightnessFile = Gio.File.new_for_path(BRIGHTNESS_PATH);
|
||||
const rgbFile = Gio.File.new_for_path(RGB_MODE_PATH);
|
||||
|
||||
return brightnessFile.query_exists(null) && rgbFile.query_exists(null);
|
||||
} catch (e) {
|
||||
console.error('Erreur lors de la vérification du matériel:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur a les permissions d'écriture
|
||||
* @returns {boolean} true si les permissions sont OK
|
||||
*/
|
||||
export function checkPermissions() {
|
||||
try {
|
||||
const brightnessFile = Gio.File.new_for_path(BRIGHTNESS_PATH);
|
||||
const info = brightnessFile.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
|
||||
|
||||
// Vérifier si on peut écrire
|
||||
return info.get_attribute_boolean('access::can-write');
|
||||
} catch (e) {
|
||||
console.error('Erreur lors de la vérification des permissions:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit la valeur maximale de brightness
|
||||
* @returns {number} Valeur max, ou 3 par défaut
|
||||
*/
|
||||
export function getMaxBrightness() {
|
||||
try {
|
||||
const file = Gio.File.new_for_path(MAX_BRIGHTNESS_PATH);
|
||||
const [success, contents] = file.load_contents(null);
|
||||
|
||||
if (success) {
|
||||
const maxValue = parseInt(new TextDecoder().decode(contents).trim());
|
||||
|
||||
// Initialiser les niveaux de brightness
|
||||
BRIGHTNESS_LEVELS[1] = Math.floor(maxValue / 3);
|
||||
BRIGHTNESS_LEVELS[2] = Math.floor(2 * maxValue / 3);
|
||||
BRIGHTNESS_LEVELS[3] = maxValue;
|
||||
|
||||
return maxValue;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur lors de la lecture de max_brightness:', e);
|
||||
}
|
||||
|
||||
// Valeurs par défaut si échec
|
||||
BRIGHTNESS_LEVELS[1] = 1;
|
||||
BRIGHTNESS_LEVELS[2] = 2;
|
||||
BRIGHTNESS_LEVELS[3] = 3;
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit la brightness actuelle
|
||||
* @returns {number} Valeur actuelle, ou -1 en cas d'erreur
|
||||
*/
|
||||
export function readBrightness() {
|
||||
try {
|
||||
const file = Gio.File.new_for_path(BRIGHTNESS_PATH);
|
||||
const [success, contents] = file.load_contents(null);
|
||||
|
||||
if (success) {
|
||||
return parseInt(new TextDecoder().decode(contents).trim());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur lors de la lecture de brightness:', e);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Écrit la brightness (niveau 0-3)
|
||||
* @param {number} level - Niveau d'intensité (0=Off, 1=Faible, 2=Moyen, 3=Fort)
|
||||
* @returns {boolean} true si succès
|
||||
*/
|
||||
export function writeBrightness(level) {
|
||||
try {
|
||||
// Valider et clamper le niveau
|
||||
level = Math.max(0, Math.min(3, Math.floor(level)));
|
||||
const value = BRIGHTNESS_LEVELS[level];
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
console.error('Niveau de brightness invalide:', level);
|
||||
return false;
|
||||
}
|
||||
|
||||
const file = Gio.File.new_for_path(BRIGHTNESS_PATH);
|
||||
const contents = `${value}\n`;
|
||||
|
||||
const [success] = file.replace_contents(
|
||||
new TextEncoder().encode(contents),
|
||||
null,
|
||||
false,
|
||||
Gio.FileCreateFlags.NONE,
|
||||
null
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log(`Brightness mise à ${level} (${value})`);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (e) {
|
||||
console.error('Erreur lors de l\'écriture de brightness:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp une valeur RGB entre 0 et 255
|
||||
* @param {number} value - Valeur à clamper
|
||||
* @returns {number} Valeur clampée
|
||||
*/
|
||||
function clampRGB(value) {
|
||||
return Math.max(0, Math.min(255, Math.floor(value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique le master gain aux valeurs RGB
|
||||
* @param {number} r - Rouge (0-255)
|
||||
* @param {number} g - Vert (0-255)
|
||||
* @param {number} b - Bleu (0-255)
|
||||
* @param {number} masterGain - Gain master (0-100)
|
||||
* @returns {Object} {r, g, b} avec gain appliqué
|
||||
*/
|
||||
function applyMasterGain(r, g, b, masterGain) {
|
||||
const gain = masterGain / 100.0;
|
||||
return {
|
||||
r: clampRGB(r * gain),
|
||||
g: clampRGB(g * gain),
|
||||
b: clampRGB(b * gain)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Écrit les valeurs RGB dans kbd_rgb_mode
|
||||
* @param {number} r - Rouge (0-255)
|
||||
* @param {number} g - Vert (0-255)
|
||||
* @param {number} b - Bleu (0-255)
|
||||
* @param {number} masterGain - Gain master (0-100), défaut 100
|
||||
* @returns {boolean} true si succès
|
||||
*/
|
||||
export function writeRGB(r, g, b, masterGain = 100) {
|
||||
try {
|
||||
// Clamper les valeurs
|
||||
r = clampRGB(r);
|
||||
g = clampRGB(g);
|
||||
b = clampRGB(b);
|
||||
|
||||
// Appliquer le master gain
|
||||
const adjusted = applyMasterGain(r, g, b, masterGain);
|
||||
|
||||
// Vérifier si brightness est > 0, sinon ne pas écrire
|
||||
const currentBrightness = readBrightness();
|
||||
if (currentBrightness === 0) {
|
||||
console.log('Brightness est 0, RGB mémorisé mais non appliqué');
|
||||
return true; // On considère cela comme un succès
|
||||
}
|
||||
|
||||
// Format: "1 0 R G B 0\n"
|
||||
const file = Gio.File.new_for_path(RGB_MODE_PATH);
|
||||
const contents = `1 0 ${adjusted.r} ${adjusted.g} ${adjusted.b} 0\n`;
|
||||
|
||||
const [success] = file.replace_contents(
|
||||
new TextEncoder().encode(contents),
|
||||
null,
|
||||
false,
|
||||
Gio.FileCreateFlags.NONE,
|
||||
null
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log(`RGB mis à (${adjusted.r}, ${adjusted.g}, ${adjusted.b}) [master: ${masterGain}%]`);
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (e) {
|
||||
console.error('Erreur lors de l\'écriture RGB:', e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique RGB avec debouncing pour éviter de spammer sysfs
|
||||
* @param {number} r - Rouge
|
||||
* @param {number} g - Vert
|
||||
* @param {number} b - Bleu
|
||||
* @param {number} masterGain - Gain master
|
||||
* @param {Function} callback - Callback optionnel appelé après l'écriture
|
||||
*/
|
||||
export function writeRGBDebounced(r, g, b, masterGain, callback = null) {
|
||||
// Annuler le timer précédent
|
||||
if (debounceTimer !== null) {
|
||||
GLib.source_remove(debounceTimer);
|
||||
debounceTimer = null;
|
||||
}
|
||||
|
||||
// Créer un nouveau timer
|
||||
debounceTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DEBOUNCE_DELAY, () => {
|
||||
writeRGB(r, g, b, masterGain);
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
debounceTimer = null;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse une chaîne preset "R,G,B" en objet
|
||||
* @param {string} presetString - Format "R,G,B"
|
||||
* @returns {Object} {r, g, b} ou null si invalide
|
||||
*/
|
||||
export function parsePreset(presetString) {
|
||||
try {
|
||||
const parts = presetString.split(',').map(s => parseInt(s.trim()));
|
||||
|
||||
if (parts.length === 3 && parts.every(n => !isNaN(n))) {
|
||||
return {
|
||||
r: clampRGB(parts[0]),
|
||||
g: clampRGB(parts[1]),
|
||||
b: clampRGB(parts[2])
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur lors du parsing du preset:', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit RGB en hex
|
||||
* @param {number} r - Rouge (0-255)
|
||||
* @param {number} g - Vert (0-255)
|
||||
* @param {number} b - Bleu (0-255)
|
||||
* @returns {string} Format "#RRGGBB"
|
||||
*/
|
||||
export function rgbToHex(r, g, b) {
|
||||
const toHex = (n) => {
|
||||
const hex = clampRGB(n).toString(16).toUpperCase();
|
||||
return hex.length === 1 ? '0' + hex : hex;
|
||||
};
|
||||
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie les ressources (annule les timers en cours)
|
||||
*/
|
||||
export function cleanup() {
|
||||
if (debounceTimer !== null) {
|
||||
GLib.source_remove(debounceTimer);
|
||||
debounceTimer = null;
|
||||
}
|
||||
}
|
||||
59
extension/extension.js
Normal file
59
extension/extension.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// extension.js - Point d'entrée de l'extension GNOME Shell ASUS RGB Keyboard
|
||||
import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
|
||||
import {KeyboardRGBIndicator} from './ui.js';
|
||||
|
||||
/**
|
||||
* Extension ASUS Keyboard RGB Control
|
||||
*/
|
||||
export default class AsusKeyboardRGBExtension extends Extension {
|
||||
constructor(metadata) {
|
||||
super(metadata);
|
||||
this._indicator = null;
|
||||
this._settings = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active l'extension
|
||||
*/
|
||||
enable() {
|
||||
console.log('Activation de l\'extension ASUS Keyboard RGB');
|
||||
|
||||
try {
|
||||
// Charger les settings
|
||||
this._settings = this.getSettings();
|
||||
|
||||
// Créer l'indicateur dans le panneau
|
||||
this._indicator = new KeyboardRGBIndicator(this._settings);
|
||||
|
||||
// Ajouter au panneau (à droite, avant les menus système)
|
||||
Main.panel.addToStatusArea(
|
||||
this.metadata.uuid,
|
||||
this._indicator,
|
||||
1,
|
||||
'right'
|
||||
);
|
||||
|
||||
console.log('Extension ASUS Keyboard RGB activée avec succès');
|
||||
} catch (e) {
|
||||
console.error('Erreur lors de l\'activation de l\'extension:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Désactive l'extension
|
||||
*/
|
||||
disable() {
|
||||
console.log('Désactivation de l\'extension ASUS Keyboard RGB');
|
||||
|
||||
if (this._indicator) {
|
||||
this._indicator.destroy();
|
||||
this._indicator = null;
|
||||
}
|
||||
|
||||
this._settings = null;
|
||||
|
||||
console.log('Extension ASUS Keyboard RGB désactivée');
|
||||
}
|
||||
}
|
||||
11
extension/metadata.json
Normal file
11
extension/metadata.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"uuid": "asus-kbd-rgb@gilles",
|
||||
"name": "ASUS Keyboard RGB Control",
|
||||
"description": "Contrôle du rétroéclairage RGB du clavier ASUS via asus-nb-wmi",
|
||||
"version": 1,
|
||||
"shell-version": [
|
||||
"48"
|
||||
],
|
||||
"url": "https://github.com/gilles/gnome-asus-kbd-rgb",
|
||||
"settings-schema": "org.gnome.shell.extensions.asuskbdrgb"
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist>
|
||||
<schema id="org.gnome.shell.extensions.asuskbdrgb" path="/org/gnome/shell/extensions/asuskbdrgb/">
|
||||
|
||||
<!-- Valeurs RGB courantes -->
|
||||
<key name="red" type="i">
|
||||
<default>255</default>
|
||||
<summary>Composante rouge (0-255)</summary>
|
||||
<description>Valeur de la composante rouge du rétroéclairage</description>
|
||||
</key>
|
||||
|
||||
<key name="green" type="i">
|
||||
<default>165</default>
|
||||
<summary>Composante verte (0-255)</summary>
|
||||
<description>Valeur de la composante verte du rétroéclairage</description>
|
||||
</key>
|
||||
|
||||
<key name="blue" type="i">
|
||||
<default>0</default>
|
||||
<summary>Composante bleue (0-255)</summary>
|
||||
<description>Valeur de la composante bleue du rétroéclairage</description>
|
||||
</key>
|
||||
|
||||
<!-- Intensité -->
|
||||
<key name="brightness-level" type="i">
|
||||
<default>2</default>
|
||||
<summary>Niveau d'intensité (0-3)</summary>
|
||||
<description>Niveau d'intensité du rétroéclairage (0=Off, 1=Faible, 2=Moyen, 3=Fort)</description>
|
||||
</key>
|
||||
|
||||
<!-- Master slider -->
|
||||
<key name="master-gain" type="i">
|
||||
<default>100</default>
|
||||
<summary>Gain master (0-100)</summary>
|
||||
<description>Pourcentage de gain appliqué à toutes les composantes RGB</description>
|
||||
</key>
|
||||
|
||||
<!-- Step pour les sliders RGB -->
|
||||
<key name="rgb-step" type="i">
|
||||
<default>5</default>
|
||||
<summary>Pas d'ajustement RGB</summary>
|
||||
<description>Incrément lors de l'utilisation des sliders RGB</description>
|
||||
</key>
|
||||
|
||||
<!-- Presets couleur (9 presets GNOME officiels, stockés comme "R,G,B") -->
|
||||
<key name="preset-1" type="s">
|
||||
<default>"53,132,228"</default>
|
||||
<summary>Preset 1 - Bleu GNOME</summary>
|
||||
</key>
|
||||
|
||||
<key name="preset-2" type="s">
|
||||
<default>"51,209,122"</default>
|
||||
<summary>Preset 2 - Turquoise</summary>
|
||||
</key>
|
||||
|
||||
<key name="preset-3" type="s">
|
||||
<default>"87,227,137"</default>
|
||||
<summary>Preset 3 - Vert</summary>
|
||||
</key>
|
||||
|
||||
<key name="preset-4" type="s">
|
||||
<default>"246,211,45"</default>
|
||||
<summary>Preset 4 - Jaune</summary>
|
||||
</key>
|
||||
|
||||
<key name="preset-5" type="s">
|
||||
<default>"255,120,0"</default>
|
||||
<summary>Preset 5 - Orange</summary>
|
||||
</key>
|
||||
|
||||
<key name="preset-6" type="s">
|
||||
<default>"237,51,59"</default>
|
||||
<summary>Preset 6 - Rouge</summary>
|
||||
</key>
|
||||
|
||||
<key name="preset-7" type="s">
|
||||
<default>"246,97,81"</default>
|
||||
<summary>Preset 7 - Rose</summary>
|
||||
</key>
|
||||
|
||||
<key name="preset-8" type="s">
|
||||
<default>"145,65,172"</default>
|
||||
<summary>Preset 8 - Violet</summary>
|
||||
</key>
|
||||
|
||||
<key name="preset-9" type="s">
|
||||
<default>"119,118,123"</default>
|
||||
<summary>Preset 9 - Gris ardoise</summary>
|
||||
</key>
|
||||
|
||||
<!-- Mode master slider (pour évolutions futures) -->
|
||||
<key name="master-mode" type="s">
|
||||
<default>"gain"</default>
|
||||
<summary>Mode du slider master</summary>
|
||||
<description>Mode de fonctionnement du slider master: gain, offset, ou hsv</description>
|
||||
</key>
|
||||
|
||||
<!-- Synchronisation avec le thème GNOME -->
|
||||
<key name="sync-gnome-theme" type="b">
|
||||
<default>false</default>
|
||||
<summary>Synchroniser avec le thème GNOME</summary>
|
||||
<description>Applique automatiquement la couleur du clavier comme couleur d'accentuation GNOME</description>
|
||||
</key>
|
||||
|
||||
</schema>
|
||||
</schemalist>
|
||||
72
extension/stylesheet.css
Normal file
72
extension/stylesheet.css
Normal file
@@ -0,0 +1,72 @@
|
||||
/* stylesheet.css - Styles pour l'extension ASUS Keyboard RGB */
|
||||
|
||||
/* Titre du menu */
|
||||
.rgb-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* Boutons d'intensité */
|
||||
.brightness-button {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
padding: 8px 16px;
|
||||
font-weight: normal;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.brightness-button:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.brightness-button.active {
|
||||
background-color: rgba(53, 132, 228, 0.8);
|
||||
border-color: rgba(53, 132, 228, 1);
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Ligne d'information */
|
||||
.info-line {
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* Boutons preset */
|
||||
.preset-button {
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.preset-button:hover {
|
||||
border-color: rgba(255, 255, 255, 0.6);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.preset-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Boîte d'erreur */
|
||||
.error-box {
|
||||
padding: 12px;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 0.9em;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.4;
|
||||
}
|
||||
852
extension/ui.js
Normal file
852
extension/ui.js
Normal file
@@ -0,0 +1,852 @@
|
||||
// ui.js - Interface utilisateur pour le contrôle RGB du clavier ASUS
|
||||
import St from 'gi://St';
|
||||
import Clutter from 'gi://Clutter';
|
||||
import GObject from 'gi://GObject';
|
||||
import Gio from 'gi://Gio';
|
||||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||||
import * as Slider from 'resource:///org/gnome/shell/ui/slider.js';
|
||||
|
||||
import * as Backend from './backend.js';
|
||||
|
||||
/**
|
||||
* Indicateur du panneau pour le contrôle RGB du clavier
|
||||
*/
|
||||
export const KeyboardRGBIndicator = GObject.registerClass(
|
||||
class KeyboardRGBIndicator extends PanelMenu.Button {
|
||||
_init(settings) {
|
||||
super._init(0.0, 'ASUS Keyboard RGB');
|
||||
|
||||
this._settings = settings;
|
||||
this._maxBrightness = Backend.getMaxBrightness();
|
||||
|
||||
// Charger les valeurs depuis GSettings
|
||||
this._currentR = this._settings.get_int('red');
|
||||
this._currentG = this._settings.get_int('green');
|
||||
this._currentB = this._settings.get_int('blue');
|
||||
this._currentMasterGain = this._settings.get_int('master-gain');
|
||||
this._currentBrightnessLevel = this._settings.get_int('brightness-level');
|
||||
|
||||
// Mode d'affichage (roue chromatique par défaut)
|
||||
this._colorPickerMode = 'wheel'; // 'sliders' ou 'wheel'
|
||||
|
||||
// Créer l'icône dans la top bar
|
||||
this._icon = new St.Icon({
|
||||
icon_name: 'keyboard-brightness-symbolic',
|
||||
style_class: 'system-status-icon'
|
||||
});
|
||||
this.add_child(this._icon);
|
||||
|
||||
// Vérifier le support matériel et les permissions
|
||||
if (!Backend.checkHardwareSupport()) {
|
||||
this._buildErrorUI('Matériel non supporté',
|
||||
'Aucun clavier ASUS RGB détecté sur ce système.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Backend.checkPermissions()) {
|
||||
this._buildErrorUI('Permissions insuffisantes',
|
||||
'Impossible d\'accéder au rétroéclairage.\nVeuillez configurer les règles udev.\n\nVoir: docs/INSTALL.md');
|
||||
return;
|
||||
}
|
||||
|
||||
// Construire l'UI normale
|
||||
this._buildUI();
|
||||
|
||||
// Restaurer l'état au démarrage
|
||||
this._applyCurrentState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit une UI d'erreur simplifiée
|
||||
*/
|
||||
_buildErrorUI(title, message) {
|
||||
const errorItem = new PopupMenu.PopupMenuItem('');
|
||||
const box = new St.BoxLayout({
|
||||
vertical: true,
|
||||
style_class: 'error-box',
|
||||
x_expand: true
|
||||
});
|
||||
|
||||
const titleLabel = new St.Label({
|
||||
text: `⚠️ ${title}`,
|
||||
style_class: 'error-title'
|
||||
});
|
||||
|
||||
const messageLabel = new St.Label({
|
||||
text: message,
|
||||
style_class: 'error-message'
|
||||
});
|
||||
|
||||
box.add_child(titleLabel);
|
||||
box.add_child(messageLabel);
|
||||
errorItem.actor.add_child(box);
|
||||
this.menu.addMenuItem(errorItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit l'interface utilisateur complète
|
||||
*/
|
||||
_buildUI() {
|
||||
// Section: Header avec titre et bouton de bascule
|
||||
this._buildHeader();
|
||||
|
||||
// Section: Boutons d'intensité (compact)
|
||||
this._buildBrightnessButtons();
|
||||
|
||||
// Section: Color picker (sliders ou roue chromatique)
|
||||
this._colorPickerContainer = new PopupMenu.PopupBaseMenuItem({
|
||||
reactive: false,
|
||||
can_focus: false
|
||||
});
|
||||
this.menu.addMenuItem(this._colorPickerContainer);
|
||||
|
||||
// Construire le mode par défaut
|
||||
this._rebuildColorPicker();
|
||||
|
||||
// Section: Presets (compact)
|
||||
this._buildPresets();
|
||||
|
||||
// Section: Synchronisation thème GNOME
|
||||
this._buildSyncThemeOption();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit le header avec titre et bouton de bascule
|
||||
*/
|
||||
_buildHeader() {
|
||||
const headerItem = new PopupMenu.PopupBaseMenuItem({
|
||||
reactive: false,
|
||||
can_focus: false,
|
||||
style_class: 'rgb-header'
|
||||
});
|
||||
|
||||
const headerBox = new St.BoxLayout({
|
||||
vertical: false,
|
||||
x_expand: true,
|
||||
style: 'spacing: 8px;'
|
||||
});
|
||||
|
||||
// Bouton de bascule slider/roue
|
||||
const toggleButton = new St.Button({
|
||||
style_class: 'button',
|
||||
x_expand: false,
|
||||
style: 'padding: 4px 8px; min-width: 32px;'
|
||||
});
|
||||
|
||||
const toggleIcon = new St.Icon({
|
||||
icon_name: this._colorPickerMode === 'sliders' ? 'view-grid-symbolic' : 'view-list-symbolic',
|
||||
icon_size: 16
|
||||
});
|
||||
toggleButton.set_child(toggleIcon);
|
||||
|
||||
toggleButton.connect('clicked', () => {
|
||||
this._colorPickerMode = this._colorPickerMode === 'sliders' ? 'wheel' : 'sliders';
|
||||
toggleIcon.icon_name = this._colorPickerMode === 'sliders' ? 'view-grid-symbolic' : 'view-list-symbolic';
|
||||
this._rebuildColorPicker();
|
||||
});
|
||||
|
||||
// Titre
|
||||
const titleLabel = new St.Label({
|
||||
text: 'Clavier RGB',
|
||||
style: 'font-weight: bold;',
|
||||
x_expand: true,
|
||||
y_align: Clutter.ActorAlign.CENTER
|
||||
});
|
||||
|
||||
// Aperçu de couleur (déplacé dans le header)
|
||||
this._colorPreview = new St.Bin({
|
||||
style_class: 'color-preview',
|
||||
style: 'width: 40px; height: 24px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.3);'
|
||||
});
|
||||
|
||||
headerBox.add_child(toggleButton);
|
||||
headerBox.add_child(titleLabel);
|
||||
headerBox.add_child(this._colorPreview);
|
||||
|
||||
headerItem.actor.add_child(headerBox);
|
||||
this.menu.addMenuItem(headerItem);
|
||||
|
||||
// Initialiser l'aperçu
|
||||
this._updateColorPreview();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruit le color picker selon le mode actuel
|
||||
*/
|
||||
_rebuildColorPicker() {
|
||||
// Vider le container
|
||||
this._colorPickerContainer.actor.remove_all_children();
|
||||
|
||||
if (this._colorPickerMode === 'sliders') {
|
||||
this._buildRGBSliders();
|
||||
} else {
|
||||
this._buildColorWheel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit les 4 boutons d'intensité (version compacte)
|
||||
*/
|
||||
_buildBrightnessButtons() {
|
||||
const buttonItem = new PopupMenu.PopupBaseMenuItem({
|
||||
reactive: false,
|
||||
can_focus: false
|
||||
});
|
||||
|
||||
const container = new St.BoxLayout({
|
||||
vertical: false,
|
||||
x_expand: true,
|
||||
style: 'spacing: 8px;'
|
||||
});
|
||||
|
||||
// Label compact
|
||||
const intensityLabel = new St.Label({
|
||||
text: 'Intensité :',
|
||||
style: 'font-weight: bold; font-size: 0.9em;',
|
||||
y_align: Clutter.ActorAlign.CENTER
|
||||
});
|
||||
container.add_child(intensityLabel);
|
||||
|
||||
// Boutons compacts
|
||||
const buttonBox = new St.BoxLayout({
|
||||
vertical: false,
|
||||
x_expand: true,
|
||||
style: 'spacing: 6px;'
|
||||
});
|
||||
|
||||
const buttonLabels = ['OFF', '1', '2', '3'];
|
||||
this._brightnessButtons = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const button = new St.Button({
|
||||
label: buttonLabels[i],
|
||||
style_class: 'brightness-button',
|
||||
x_expand: true,
|
||||
can_focus: true,
|
||||
style: 'padding: 4px 8px; font-size: 0.9em;'
|
||||
});
|
||||
|
||||
button.level = i;
|
||||
button.connect('clicked', () => this._onBrightnessButtonClicked(i));
|
||||
|
||||
this._brightnessButtons.push(button);
|
||||
buttonBox.add_child(button);
|
||||
}
|
||||
|
||||
container.add_child(buttonBox);
|
||||
buttonItem.actor.add_child(container);
|
||||
this.menu.addMenuItem(buttonItem);
|
||||
|
||||
// Mettre à jour l'état actif
|
||||
this._updateBrightnessButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le style des boutons brightness (active/inactive)
|
||||
*/
|
||||
_updateBrightnessButtons() {
|
||||
this._brightnessButtons.forEach((button, index) => {
|
||||
if (index === this._currentBrightnessLevel) {
|
||||
button.add_style_class_name('active');
|
||||
} else {
|
||||
button.remove_style_class_name('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback: clic sur un bouton brightness
|
||||
*/
|
||||
_onBrightnessButtonClicked(level) {
|
||||
this._currentBrightnessLevel = level;
|
||||
this._settings.set_int('brightness-level', level);
|
||||
|
||||
Backend.writeBrightness(level);
|
||||
this._updateBrightnessButtons();
|
||||
this._updateInfoLine();
|
||||
|
||||
// Si brightness > 0, réappliquer RGB
|
||||
if (level > 0) {
|
||||
Backend.writeRGB(this._currentR, this._currentG, this._currentB, this._currentMasterGain);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit les sliders RGB + Master (version compacte)
|
||||
*/
|
||||
_buildRGBSliders() {
|
||||
const container = new St.BoxLayout({
|
||||
vertical: true,
|
||||
x_expand: true,
|
||||
style: 'spacing: 4px; padding: 4px;'
|
||||
});
|
||||
|
||||
// Sliders RGB compacts
|
||||
const sliders = [
|
||||
{ label: 'R', color: '#ff4444', prop: '_currentR', key: 'red', max: 255 },
|
||||
{ label: 'V', color: '#44ff44', prop: '_currentG', key: 'green', max: 255 },
|
||||
{ label: 'B', color: '#4444ff', prop: '_currentB', key: 'blue', max: 255 },
|
||||
{ label: 'M', color: '#ffffff', prop: '_currentMasterGain', key: 'master-gain', max: 100 }
|
||||
];
|
||||
|
||||
sliders.forEach(({ label, color, prop, key, max }) => {
|
||||
const sliderBox = new St.BoxLayout({
|
||||
vertical: false,
|
||||
x_expand: true,
|
||||
style: 'spacing: 8px;'
|
||||
});
|
||||
|
||||
const sliderLabel = new St.Label({
|
||||
text: label,
|
||||
style: `min-width: 12px; color: ${color}; font-weight: bold; font-size: 0.9em;`,
|
||||
y_align: Clutter.ActorAlign.CENTER
|
||||
});
|
||||
|
||||
const slider = new Slider.Slider(this[prop] / max);
|
||||
slider.accessible_name = label;
|
||||
|
||||
slider.connect('notify::value', () => {
|
||||
const newValue = Math.floor(slider.value * max);
|
||||
this[prop] = newValue;
|
||||
this._settings.set_int(key, newValue);
|
||||
this._onRGBChanged();
|
||||
});
|
||||
|
||||
// Stocker la référence
|
||||
if (label === 'R') this._redSlider = slider;
|
||||
else if (label === 'V') this._greenSlider = slider;
|
||||
else if (label === 'B') this._blueSlider = slider;
|
||||
else if (label === 'M') this._masterSlider = slider;
|
||||
|
||||
sliderBox.add_child(sliderLabel);
|
||||
sliderBox.add_child(slider);
|
||||
container.add_child(sliderBox);
|
||||
});
|
||||
|
||||
this._colorPickerContainer.actor.add_child(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit la roue chromatique (grille de couleurs cliquables)
|
||||
*/
|
||||
_buildColorWheel() {
|
||||
const container = new St.BoxLayout({
|
||||
vertical: true,
|
||||
x_expand: true,
|
||||
style: 'spacing: 8px; padding: 8px;'
|
||||
});
|
||||
|
||||
// Grille de couleurs en forme de cercle (optimisée pour la visibilité)
|
||||
const gridBox = new St.BoxLayout({
|
||||
vertical: true,
|
||||
style: 'spacing: 3px;'
|
||||
});
|
||||
|
||||
const size = 12; // Grille 12x12 (réduit pour boutons plus grands)
|
||||
const cellSize = 18; // Augmenté de 12px à 18px
|
||||
const centerX = size / 2;
|
||||
const centerY = size / 2;
|
||||
const maxRadius = size / 2 - 1;
|
||||
|
||||
// Stocker les boutons de la roue pour la surbrillance
|
||||
this._wheelButtons = [];
|
||||
|
||||
for (let y = 0; y < size; y++) {
|
||||
const row = new St.BoxLayout({
|
||||
vertical: false,
|
||||
style: 'spacing: 3px;'
|
||||
});
|
||||
|
||||
for (let x = 0; x < size; x++) {
|
||||
const dx = x - centerX;
|
||||
const dy = y - centerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance <= maxRadius) {
|
||||
// Calculer teinte et saturation
|
||||
const angle = Math.atan2(dy, dx);
|
||||
const hue = ((angle * 180 / Math.PI) + 360) % 360;
|
||||
const saturation = distance / maxRadius;
|
||||
|
||||
// Convertir HSL vers RGB
|
||||
const rgb = this._hslToRgb(hue, saturation, 0.5);
|
||||
const hex = Backend.rgbToHex(rgb.r, rgb.g, rgb.b);
|
||||
|
||||
const button = new St.Button({
|
||||
style: `
|
||||
background-color: ${hex};
|
||||
width: ${cellSize}px;
|
||||
height: ${cellSize}px;
|
||||
border-radius: 3px;
|
||||
padding: 0;
|
||||
min-width: ${cellSize}px;
|
||||
min-height: ${cellSize}px;
|
||||
border: 1px solid rgba(0,0,0,0.3);
|
||||
`,
|
||||
reactive: true,
|
||||
track_hover: true
|
||||
});
|
||||
|
||||
// Stocker les infos de couleur pour comparaison
|
||||
button._rgb = { r: rgb.r, g: rgb.g, b: rgb.b };
|
||||
button._baseStyle = `
|
||||
background-color: ${hex};
|
||||
width: ${cellSize}px;
|
||||
height: ${cellSize}px;
|
||||
border-radius: 3px;
|
||||
padding: 0;
|
||||
min-width: ${cellSize}px;
|
||||
min-height: ${cellSize}px;
|
||||
border: 1px solid rgba(0,0,0,0.3);
|
||||
`;
|
||||
|
||||
button.connect('clicked', () => {
|
||||
this._currentR = rgb.r;
|
||||
this._currentG = rgb.g;
|
||||
this._currentB = rgb.b;
|
||||
|
||||
this._settings.set_int('red', this._currentR);
|
||||
this._settings.set_int('green', this._currentG);
|
||||
this._settings.set_int('blue', this._currentB);
|
||||
|
||||
// Mettre à jour la surbrillance
|
||||
this._updateWheelSelection();
|
||||
|
||||
this._onRGBChanged();
|
||||
});
|
||||
|
||||
this._wheelButtons.push(button);
|
||||
row.add_child(button);
|
||||
} else {
|
||||
// Espace vide en dehors du cercle
|
||||
const spacer = new St.Bin({
|
||||
style: `width: ${cellSize}px; height: ${cellSize}px;`
|
||||
});
|
||||
row.add_child(spacer);
|
||||
}
|
||||
}
|
||||
|
||||
gridBox.add_child(row);
|
||||
}
|
||||
|
||||
container.add_child(gridBox);
|
||||
|
||||
// Slider Master en dessous
|
||||
const masterBox = new St.BoxLayout({
|
||||
vertical: false,
|
||||
x_expand: true,
|
||||
style: 'spacing: 8px; margin-top: 8px;'
|
||||
});
|
||||
|
||||
const masterLabel = new St.Label({
|
||||
text: 'Luminosité',
|
||||
style: 'font-weight: bold; font-size: 0.9em; min-width: 60px;',
|
||||
y_align: Clutter.ActorAlign.CENTER
|
||||
});
|
||||
|
||||
const masterSlider = new Slider.Slider(this._currentMasterGain / 100);
|
||||
masterSlider.accessible_name = 'Master';
|
||||
|
||||
masterSlider.connect('notify::value', () => {
|
||||
this._currentMasterGain = Math.floor(masterSlider.value * 100);
|
||||
this._settings.set_int('master-gain', this._currentMasterGain);
|
||||
this._onRGBChanged();
|
||||
});
|
||||
|
||||
this._masterSlider = masterSlider;
|
||||
|
||||
masterBox.add_child(masterLabel);
|
||||
masterBox.add_child(masterSlider);
|
||||
container.add_child(masterBox);
|
||||
|
||||
this._colorPickerContainer.actor.add_child(container);
|
||||
|
||||
// Initialiser la surbrillance de la couleur actuelle
|
||||
this._updateWheelSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la surbrillance de la couleur sélectionnée dans la roue
|
||||
*/
|
||||
_updateWheelSelection() {
|
||||
if (!this._wheelButtons) return;
|
||||
|
||||
this._wheelButtons.forEach(button => {
|
||||
const rgb = button._rgb;
|
||||
// Comparer avec une tolérance (les couleurs HSL peuvent varier légèrement)
|
||||
const tolerance = 10;
|
||||
const matches =
|
||||
Math.abs(rgb.r - this._currentR) <= tolerance &&
|
||||
Math.abs(rgb.g - this._currentG) <= tolerance &&
|
||||
Math.abs(rgb.b - this._currentB) <= tolerance;
|
||||
|
||||
if (matches) {
|
||||
// Bordure blanche épaisse pour la sélection
|
||||
button.style = button._baseStyle.replace(
|
||||
'border: 1px solid rgba(0,0,0,0.3)',
|
||||
'border: 3px solid white; box-shadow: 0 0 5px rgba(255,255,255,0.8)'
|
||||
);
|
||||
} else {
|
||||
// Style normal
|
||||
button.style = button._baseStyle;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la surbrillance du preset sélectionné
|
||||
*/
|
||||
_updatePresetSelection() {
|
||||
if (!this._presetButtons) return;
|
||||
|
||||
this._presetButtons.forEach(button => {
|
||||
const preset = button._preset;
|
||||
// Comparer avec une tolérance
|
||||
const tolerance = 10;
|
||||
const matches =
|
||||
Math.abs(preset.r - this._currentR) <= tolerance &&
|
||||
Math.abs(preset.g - this._currentG) <= tolerance &&
|
||||
Math.abs(preset.b - this._currentB) <= tolerance;
|
||||
|
||||
if (matches) {
|
||||
// Cercle blanc épais autour du preset sélectionné
|
||||
button.style = button._baseStyle + ' border: 3px solid white; box-shadow: 0 0 5px rgba(255,255,255,0.8);';
|
||||
} else {
|
||||
// Style normal
|
||||
button.style = button._baseStyle + ' border: 2px solid rgba(255,255,255,0.3);';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve la couleur accent GNOME la plus proche d'une couleur RGB
|
||||
*/
|
||||
_rgbToGnomeAccent(r, g, b) {
|
||||
const colors = {
|
||||
blue: { r: 53, g: 132, b: 228 }, // Bleu GNOME
|
||||
teal: { r: 51, g: 209, b: 122 }, // Turquoise
|
||||
green: { r: 87, g: 227, b: 137 }, // Vert
|
||||
yellow: { r: 246, g: 211, b: 45 }, // Jaune
|
||||
orange: { r: 255, g: 120, b: 0 }, // Orange
|
||||
red: { r: 237, g: 51, b: 59 }, // Rouge
|
||||
pink: { r: 246, g: 97, b: 81 }, // Rose
|
||||
purple: { r: 145, g: 65, b: 172 }, // Violet
|
||||
slate: { r: 119, g: 118, b: 123 } // Gris ardoise
|
||||
};
|
||||
|
||||
let minDistance = Infinity;
|
||||
let closestColor = 'blue';
|
||||
|
||||
// Calculer la distance euclidienne dans l'espace RGB
|
||||
for (const [name, color] of Object.entries(colors)) {
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(r - color.r, 2) +
|
||||
Math.pow(g - color.g, 2) +
|
||||
Math.pow(b - color.b, 2)
|
||||
);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestColor = name;
|
||||
}
|
||||
}
|
||||
|
||||
return closestColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise la couleur du clavier avec le thème GNOME
|
||||
*/
|
||||
_syncGnomeTheme() {
|
||||
// Vérifier si la synchronisation est activée
|
||||
if (!this._settings.get_boolean('sync-gnome-theme')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Trouver la couleur accent GNOME la plus proche
|
||||
const accentColor = this._rgbToGnomeAccent(
|
||||
this._currentR,
|
||||
this._currentG,
|
||||
this._currentB
|
||||
);
|
||||
|
||||
try {
|
||||
// Appliquer la couleur d'accentuation GNOME
|
||||
const interfaceSettings = new Gio.Settings({
|
||||
schema: 'org.gnome.desktop.interface'
|
||||
});
|
||||
interfaceSettings.set_string('accent-color', accentColor);
|
||||
|
||||
log(`[ASUS RGB] Thème GNOME synchronisé → ${accentColor} (RGB: ${this._currentR}, ${this._currentG}, ${this._currentB})`);
|
||||
} catch (error) {
|
||||
logError(error, '[ASUS RGB] Erreur lors de la synchronisation du thème GNOME');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback: RGB ou Master a changé
|
||||
*/
|
||||
_onRGBChanged() {
|
||||
// Mettre à jour l'aperçu de couleur
|
||||
this._updateColorPreview();
|
||||
|
||||
// Mettre à jour les surbrillances
|
||||
this._updateWheelSelection();
|
||||
this._updatePresetSelection();
|
||||
|
||||
// Synchroniser avec le thème GNOME si activé
|
||||
this._syncGnomeTheme();
|
||||
|
||||
// Appliquer avec debouncing
|
||||
Backend.writeRGBDebounced(
|
||||
this._currentR,
|
||||
this._currentG,
|
||||
this._currentB,
|
||||
this._currentMasterGain,
|
||||
() => this._updateInfoLine()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit RGB (0-255) vers HSL (H: 0-360, S: 0-1, L: 0-1)
|
||||
*/
|
||||
_rgbToHsl(r, g, b) {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0; // Gris (pas de saturation)
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
|
||||
return { h: h * 360, s: s, l: l };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit HSL (H: 0-360, S: 0-1, L: 0-1) vers RGB (0-255)
|
||||
*/
|
||||
_hslToRgb(h, s, l) {
|
||||
h /= 360;
|
||||
|
||||
let r, g, b;
|
||||
|
||||
if (s === 0) {
|
||||
r = g = b = l; // Gris
|
||||
} else {
|
||||
const hue2rgb = (p, q, t) => {
|
||||
if (t < 0) t += 1;
|
||||
if (t > 1) t -= 1;
|
||||
if (t < 1/6) return p + (q - p) * 6 * t;
|
||||
if (t < 1/2) return q;
|
||||
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||
return p;
|
||||
};
|
||||
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
|
||||
r = hue2rgb(p, q, h + 1/3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1/3);
|
||||
}
|
||||
|
||||
return {
|
||||
r: Math.round(r * 255),
|
||||
g: Math.round(g * 255),
|
||||
b: Math.round(b * 255)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour l'aperçu de couleur (version HSL avec correction gamma)
|
||||
*/
|
||||
_updateColorPreview() {
|
||||
if (this._colorPreview) {
|
||||
// Convertir RGB vers HSL
|
||||
const hsl = this._rgbToHsl(this._currentR, this._currentG, this._currentB);
|
||||
|
||||
// Appliquer correction gamma pour progression perceptuelle naturelle
|
||||
// Gamma 2.2 est la norme pour les écrans sRGB
|
||||
const GAMMA = 2.2;
|
||||
const gain = this._currentMasterGain / 100.0;
|
||||
|
||||
// Linéariser la luminosité, appliquer le gain, puis re-gamma
|
||||
const linearL = Math.pow(hsl.l, GAMMA);
|
||||
const adjustedLinearL = linearL * gain;
|
||||
hsl.l = Math.pow(adjustedLinearL, 1 / GAMMA);
|
||||
|
||||
// Reconvertir HSL vers RGB
|
||||
const rgb = this._hslToRgb(hsl.h, hsl.s, hsl.l);
|
||||
|
||||
const hex = Backend.rgbToHex(rgb.r, rgb.g, rgb.b);
|
||||
this._colorPreview.style = `background-color: ${hex}; width: 50px; height: 30px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.3);`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit la ligne d'information
|
||||
*/
|
||||
/**
|
||||
* Met à jour la ligne d'information (vide - infos dans l'aperçu)
|
||||
*/
|
||||
_updateInfoLine() {
|
||||
// Les informations sont maintenant affichées via l'aperçu de couleur dans le header
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit les 6 presets couleur
|
||||
*/
|
||||
/**
|
||||
* Construit les presets de couleurs (version compacte)
|
||||
*/
|
||||
_buildPresets() {
|
||||
const presetItem = new PopupMenu.PopupBaseMenuItem({
|
||||
reactive: false,
|
||||
can_focus: false
|
||||
});
|
||||
|
||||
const container = new St.BoxLayout({
|
||||
vertical: false,
|
||||
x_expand: true,
|
||||
style: 'spacing: 6px;'
|
||||
});
|
||||
|
||||
const presetLabel = new St.Label({
|
||||
text: 'Presets :',
|
||||
style: 'font-weight: bold; font-size: 0.9em;',
|
||||
y_align: Clutter.ActorAlign.CENTER
|
||||
});
|
||||
container.add_child(presetLabel);
|
||||
|
||||
// Créer 9 boutons preset compacts (couleurs GNOME) - ronds
|
||||
const buttonBox = new St.BoxLayout({
|
||||
vertical: false,
|
||||
x_expand: true,
|
||||
style: 'spacing: 3px;'
|
||||
});
|
||||
|
||||
// Stocker les boutons pour la mise à jour de la sélection
|
||||
this._presetButtons = [];
|
||||
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
const presetString = this._settings.get_string(`preset-${i}`);
|
||||
const preset = Backend.parsePreset(presetString);
|
||||
|
||||
if (preset) {
|
||||
const button = new St.Button({
|
||||
style_class: 'button',
|
||||
x_expand: true,
|
||||
can_focus: true
|
||||
});
|
||||
|
||||
const hex = Backend.rgbToHex(preset.r, preset.g, preset.b);
|
||||
// Ronds avec border-radius 50%
|
||||
const baseStyle = `background-color: ${hex}; width: 26px; height: 26px; border-radius: 50%; padding: 0;`;
|
||||
button.style = baseStyle + ' border: 2px solid rgba(255,255,255,0.3);';
|
||||
|
||||
// Stocker les infos pour la mise à jour
|
||||
button._preset = preset;
|
||||
button._baseStyle = baseStyle;
|
||||
|
||||
button.connect('clicked', () => this._onPresetClicked(preset));
|
||||
buttonBox.add_child(button);
|
||||
this._presetButtons.push(button);
|
||||
}
|
||||
}
|
||||
|
||||
container.add_child(buttonBox);
|
||||
presetItem.actor.add_child(container);
|
||||
this.menu.addMenuItem(presetItem);
|
||||
|
||||
// Initialiser la surbrillance du preset actuel
|
||||
this._updatePresetSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit l'option de synchronisation avec le thème GNOME
|
||||
*/
|
||||
_buildSyncThemeOption() {
|
||||
const syncItem = new PopupMenu.PopupSwitchMenuItem(
|
||||
'Synchroniser thème GNOME',
|
||||
this._settings.get_boolean('sync-gnome-theme')
|
||||
);
|
||||
|
||||
syncItem.connect('toggled', (item) => {
|
||||
this._settings.set_boolean('sync-gnome-theme', item.state);
|
||||
|
||||
// Si activé, appliquer immédiatement
|
||||
if (item.state) {
|
||||
this._syncGnomeTheme();
|
||||
} else {
|
||||
// Si désactivé, restaurer la couleur GNOME par défaut (blue)
|
||||
try {
|
||||
const interfaceSettings = new Gio.Settings({
|
||||
schema: 'org.gnome.desktop.interface'
|
||||
});
|
||||
interfaceSettings.set_string('accent-color', 'blue');
|
||||
log('[ASUS RGB] Thème GNOME restauré → blue (défaut)');
|
||||
} catch (error) {
|
||||
logError(error, '[ASUS RGB] Erreur lors de la restauration du thème GNOME');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.menu.addMenuItem(syncItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback: clic sur un preset
|
||||
*/
|
||||
_onPresetClicked(preset) {
|
||||
this._currentR = preset.r;
|
||||
this._currentG = preset.g;
|
||||
this._currentB = preset.b;
|
||||
|
||||
// Sauvegarder
|
||||
this._settings.set_int('red', this._currentR);
|
||||
this._settings.set_int('green', this._currentG);
|
||||
this._settings.set_int('blue', this._currentB);
|
||||
|
||||
// Mettre à jour les sliders si en mode sliders
|
||||
if (this._redSlider) {
|
||||
this._redSlider.value = this._currentR / 255;
|
||||
this._greenSlider.value = this._currentG / 255;
|
||||
this._blueSlider.value = this._currentB / 255;
|
||||
}
|
||||
|
||||
// Appliquer via _onRGBChanged qui gère tout (aperçu, surbrillances, sync thème, etc.)
|
||||
this._onRGBChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique l'état actuel au matériel (au démarrage)
|
||||
*/
|
||||
_applyCurrentState() {
|
||||
Backend.writeBrightness(this._currentBrightnessLevel);
|
||||
|
||||
if (this._currentBrightnessLevel > 0) {
|
||||
Backend.writeRGB(this._currentR, this._currentG, this._currentB, this._currentMasterGain);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoyage lors de la destruction
|
||||
*/
|
||||
destroy() {
|
||||
Backend.cleanup();
|
||||
super.destroy();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user