853 lines
28 KiB
JavaScript
853 lines
28 KiB
JavaScript
// 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();
|
|
}
|
|
});
|