diff options
Diffstat (limited to 'raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm')
9 files changed, 1477 insertions, 0 deletions
diff --git a/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/extension.js b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/extension.js new file mode 100644 index 0000000..b188440 --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/extension.js @@ -0,0 +1,580 @@ +import Atk from "gi://Atk"; +import Clutter from "gi://Clutter"; +import Meta from "gi://Meta"; +import St from "gi://St"; + +import Gio from 'gi://Gio'; +import GObject from 'gi://GObject'; +import GLib from 'gi://GLib'; +import Shell from 'gi://Shell'; + +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as AltTab from 'resource:///org/gnome/shell/ui/altTab.js'; +import * as SwitcherPopup from 'resource:///org/gnome/shell/ui/switcherPopup.js'; + +import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js'; + +import * as Utils from './utils.js'; + + +const baseIconSizes = [96, 64, 48, 32, 22]; + + +let injections = {}; +let extensionInstance = null; + +function getWindows(workspace) { + let windows = global.display.get_tab_list(Meta.TabList.NORMAL_ALL, workspace); + return windows.map(w => { + return w.is_attached_dialog() ? w.get_transient_for() : w; + }).filter((w, i, a) => w && !w.skip_taskbar && a.indexOf(w) === i); +} + +function _finish(timestamp) { + this._currentWindow = this._currentWindow < 0 ? 0 : this._currentWindow; + return injections._finish.call(this, timestamp); +} + +function _initialSelection(backward, binding) { + if (backward || binding != 'switch-applications' + || this._items.length == 0 || this._items[0].cachedWindows.length < 2) { + injections._initialSelection.call(this, backward, binding); + return; + } + + let ws = global.workspace_manager.get_active_workspace(); + let wt = Shell.WindowTracker.get_default(); + let tab_list = global.display.get_tab_list(Meta.TabList.NORMAL, ws); + + if (!tab_list || tab_list.length < 2) { + injections._initialSelection.call(this, backward, binding); + return; + } + + let currentApp = wt.get_window_app(tab_list[0]); + let secondApp = wt.get_window_app(tab_list[1]); + + if (currentApp == secondApp) { + this._select(0, 1); + } else { + injections._initialSelection.call(this, backward, binding); + } +} + +function highlight2(index, justOutline) { + if (this._items[this._highlighted]) { + this._items[this._highlighted].remove_style_pseudo_class('outlined'); + this._items[this._highlighted].remove_style_pseudo_class('selected'); + } + + if (this._items[index]) { + if (justOutline) + this._items[index].add_style_pseudo_class('outlined'); + else + this._items[index].add_style_pseudo_class('selected'); + } + + this._highlighted = index; + + // GNOME 45+ compatibility: use hadjustment instead of hscroll.adjustment + let adjustment = this._scrollView.hadjustment || (this._scrollView.hscroll ? this._scrollView.hscroll.adjustment : null); + if (adjustment) { + this._scroll(index); + } +} + +function _scroll(index) { + // GNOME 45+ compatibility: use hadjustment instead of hscroll.adjustment + let adjustment = this._scrollView.hadjustment || (this._scrollView.hscroll ? this._scrollView.hscroll.adjustment : null); + if (!adjustment) return; + + let { upper, page_size: pageSize } = adjustment; + + let n = this._items.length; + let fakeSize = 2; + + this._scrollableRight = index !== n - 1; + this._scrollableLeft = index !== 0; + if (upper === pageSize) + return; + + let item = this._items[index]; + if (!item) return; + + let sizeItem = (item.allocation.x2 - item.allocation.x1); + let value = (upper - pageSize + sizeItem) * (index / n); + let maxScrollingAmount = (upper - pageSize); + let percentaje = (index - fakeSize) / (n - 1 - 2 * fakeSize); + value = percentaje * maxScrollingAmount; + + if (index < fakeSize || percentaje <= 0) { + this._scrollableLeft = false; + value = 0; + } else if (index >= n - fakeSize || percentaje >= 1) { + this._scrollableRight = false; + value = maxScrollingAmount; + } + + adjustment.ease(value, { + progress_mode: Clutter.AnimationMode.EASE_OUT_EXPO, + duration: 250, + onComplete: () => { + this.queue_relayout(); + }, + }); +} + +function addColours(settings) { + injections.WINDOW_PREVIEW_SIZE = AltTab.WINDOW_PREVIEW_SIZE; + + if (AltTab.AppSwitcherPopup) { + injections._init = AltTab.AppSwitcherPopup.prototype._init; + AltTab.AppSwitcherPopup.prototype._init = function (items, mask, action, timeout) { + // Call original + injections._init.call(this, items, mask, action, timeout); + + // Custom additions + this._thumbnails = null; + this._thumbnailTimeoutId = 0; + this._currentWindow = -1; + this.thumbnailsVisible = false; + + let apps = Shell.AppSystem.get_default().get_running(); + + // Remove the original switcher list if it exists + if (this._switcherList && typeof this._switcherList.destroy === 'function') { + try { + this._switcherList.destroy(); + } catch (e) { + // Already destroyed or other error + } + } + + this._switcherList = new AppSwitcher(apps, this, settings); + this._items = this._switcherList.icons; + + this.add_child(this._switcherList); + }; + } + + injections.highlight2 = SwitcherPopup.SwitcherList.prototype.highlight; + SwitcherPopup.SwitcherList.prototype.highlight = highlight2; + + injections._scroll = SwitcherPopup.SwitcherList.prototype._scroll; + SwitcherPopup.SwitcherList.prototype._scroll = _scroll; +} + +function removeColours() { + if (AltTab.AppSwitcherPopup && injections._init) { + AltTab.AppSwitcherPopup.prototype._init = injections._init; + } + + if (injections.highlight2) { + SwitcherPopup.SwitcherList.prototype.highlight = injections.highlight2; + injections.highlight2 = undefined; + } + + if (injections._scroll) { + SwitcherPopup.SwitcherList.prototype._scroll = injections._scroll; + injections._scroll = undefined; + } +} + +function setInitialSelection() { + if (!injections._finish && AltTab.AppSwitcherPopup) { + injections._finish = AltTab.AppSwitcherPopup.prototype._finish; + AltTab.AppSwitcherPopup.prototype._finish = _finish; + } + + if (!injections._initialSelection && AltTab.AppSwitcherPopup) { + injections._initialSelection = AltTab.AppSwitcherPopup.prototype._initialSelection; + AltTab.AppSwitcherPopup.prototype._initialSelection = _initialSelection; + } +} + +function resetInitialSelection() { + if (injections._finish && AltTab.AppSwitcherPopup) { + AltTab.AppSwitcherPopup.prototype._finish = injections._finish; + injections._finish = undefined; + } + + if (injections._initialSelection && AltTab.AppSwitcherPopup) { + AltTab.AppSwitcherPopup.prototype._initialSelection = injections._initialSelection; + injections._initialSelection = undefined; + } +} + +class MyExtension { + constructor(settings) { + this._settings = settings; + this._connectSettings(); + this._firstChangeWindowChanged(); + this._updateHoverSettings(); + } + + _connectSettings() { + this._settingsHandlerFirstSwitch = this._settings.connect( + 'changed::first-change-window', + this._firstChangeWindowChanged.bind(this) + ); + + this._settingsHandlers = []; + [ + 'hover-shade-level', 'hover-border-width', 'hover-glow-size', + 'hover-opacity', 'icon-spacing', + 'container-padding', 'background-color', 'border-size', 'border-color', + 'background-border-radius', 'icon-border-radius' + ].forEach(key => { + this._settingsHandlers.push( + this._settings.connect(`changed::${key}`, this._updateHoverSettings.bind(this)) + ); + }); + } + + _updateHoverSettings() { + const keys = this._settings.list_keys(); + this.hoverShadeLevel = keys.includes('hover-shade-level') ? this._settings.get_double('hover-shade-level') : 0.2; + this.hoverBorderWidth = keys.includes('hover-border-width') ? this._settings.get_int('hover-border-width') : 2; + this.hoverGlowSize = keys.includes('hover-glow-size') ? this._settings.get_int('hover-glow-size') : 15; + this.hoverOpacity = keys.includes('hover-opacity') ? this._settings.get_double('hover-opacity') : 0.8; + + this.iconSpacing = keys.includes('icon-spacing') ? this._settings.get_int('icon-spacing') : 10; + this.containerPadding = keys.includes('container-padding') ? this._settings.get_int('container-padding') : 24; + this.backgroundColor = keys.includes('background-color') ? this._settings.get_string('background-color') : 'rgba(0, 0, 0, 0.5)'; + this.borderSize = keys.includes('border-size') ? this._settings.get_int('border-size') : 2; + this.borderColor = keys.includes('border-color') ? this._settings.get_string('border-color') : 'rgba(255, 255, 255, 0.2)'; + this.backgroundBorderRadius = keys.includes('background-border-radius') ? this._settings.get_int('background-border-radius') : 24; + this.iconBorderRadius = keys.includes('icon-border-radius') ? this._settings.get_int('icon-border-radius') : 16; + } + + _firstChangeWindowChanged() { + this._firstChangeWindow = this._settings.get_boolean('first-change-window'); + if (this._firstChangeWindow) { + setInitialSelection(); + } else { + resetInitialSelection(); + } + } + + destroy() { + this._disconnectSettings(); + resetInitialSelection(); + } + + _disconnectSettings() { + if (this._settingsHandlerFirstSwitch) { + this._settings.disconnect(this._settingsHandlerFirstSwitch); + this._settingsHandlerFirstSwitch = 0; + } + if (this._settingsHandlers) { + this._settingsHandlers.forEach(h => this._settings.disconnect(h)); + this._settingsHandlers = []; + } + } +} + +export default class UnityLikeAppSwitcherExtension extends Extension { + enable() { + this.initTranslations("unity-window-switcher"); + const settings = this.getSettings(); + extensionInstance = new MyExtension(settings); + + addColours(settings); + } + + disable() { + removeColours(); + + if (extensionInstance) { + extensionInstance.destroy(); + extensionInstance = null; + } + } +} + +const AppSwitcher = GObject.registerClass( + class AppSwitcher extends SwitcherPopup.SwitcherList { + _init(apps, altTabPopup, extensionSettings) { + super._init(true); + + this.icons = []; + this._arrows = []; + this._extensionSettings = extensionSettings; + + let windowTracker = Shell.WindowTracker.get_default(); + let settings = new Gio.Settings({ schema_id: 'org.gnome.shell.app-switcher' }); + + let workspace = null; + if (settings.get_boolean('current-workspace-only')) { + let workspaceManager = global.workspace_manager; + workspace = workspaceManager.get_active_workspace(); + } + + let allWindows = getWindows(workspace); + + for (let i = 0; i < apps.length; i++) { + let appIcon = new AltTab.AppIcon(apps[i]); + appIcon.cachedWindows = allWindows.filter( + w => windowTracker.get_window_app(w) === appIcon.app); + if (appIcon.cachedWindows.length > 0) + this._addIcon(appIcon); + } + + this._altTabPopup = altTabPopup; + this._delayedHighlighted = -1; + this._mouseTimeOutId = 0; + + // Apply container styles + const keys = this._extensionSettings.list_keys(); + const padding = keys.includes('container-padding') ? this._extensionSettings.get_int('container-padding') : 24; + const bgColor = keys.includes('background-color') ? this._extensionSettings.get_string('background-color') : 'rgba(0, 0, 0, 0.5)'; + const borderWidth = keys.includes('border-size') ? this._extensionSettings.get_int('border-size') : 2; + const borderColor = keys.includes('border-color') ? this._extensionSettings.get_string('border-color') : 'rgba(255, 255, 255, 0.2)'; + const spacing = keys.includes('icon-spacing') ? this._extensionSettings.get_int('icon-spacing') : 10; + const borderRadius = keys.includes('background-border-radius') ? this._extensionSettings.get_int('background-border-radius') : 24; + this._iconBorderRadius = keys.includes('icon-border-radius') ? this._extensionSettings.get_int('icon-border-radius') : 16; + + this.set_style(` + padding: ${padding}px; + background-color: ${bgColor}; + border: ${borderWidth}px solid ${borderColor}; + border-radius: ${borderRadius}px; + `); + this._list.set_style(`spacing: ${spacing}px;`); + + this.connect('destroy', this._onDestroy.bind(this)); + } + + _onDestroy() { + if (this._mouseTimeOutId !== 0) + GLib.source_remove(this._mouseTimeOutId); + + this.icons.forEach( + icon => icon.app.disconnectObject(this)); + } + + _setIconSize() { + let j = 0; + while (j < this._items.length && this._items[j].style_class !== 'item-box') + j++; + + if (j >= this._items.length) return; + + let themeNode = this._items[j].get_theme_node(); + this._list.ensure_style(); + + let iconPadding = themeNode.get_horizontal_padding(); + let iconBorder = themeNode.get_border_width(St.Side.LEFT) + themeNode.get_border_width(St.Side.RIGHT); + let labelNaturalHeight = 0; + if (this.icons[j].label) { + [, labelNaturalHeight] = this.icons[j].label.get_preferred_height(-1); + } + let iconSpacing = labelNaturalHeight + iconPadding + iconBorder; + + const keys = this._extensionSettings.list_keys(); + const spacing = keys.includes('icon-spacing') ? this._extensionSettings.get_int('icon-spacing') : 10; + let totalSpacing = spacing * (this._items.length - 1); + + let primary = Main.layoutManager.primaryMonitor; + let parent = this.get_parent(); + let parentPadding = parent ? parent.get_theme_node().get_horizontal_padding() : 0; + let availWidth = primary.width - parentPadding - this.get_theme_node().get_horizontal_padding(); + + let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; + let iconSizes = baseIconSizes.map(s => s * scaleFactor); + let iconSize = baseIconSizes[0]; + + if (this._items.length > 1) { + for (let i = 0; i < baseIconSizes.length; i++) { + iconSize = baseIconSizes[i]; + let height = iconSizes[i] + iconSpacing; + let w = height * this._items.length + totalSpacing; + if (w <= availWidth) + break; + } + } + + this._iconSize = iconSize; + + for (let i = 0; i < this.icons.length; i++) { + if (this.icons[i] && this.icons[i].icon == null) { + this.icons[i].set_size(iconSize); + } + } + } + + vfunc_get_preferred_height(forWidth) { + if (!this._iconSize) + this._setIconSize(); + + return super.vfunc_get_preferred_height(forWidth); + } + + vfunc_allocate(box) { + super.vfunc_allocate(box); + + let contentBox = this.get_theme_node().get_content_box(box); + let arrowHeight = Math.floor(this.get_theme_node().get_padding(St.Side.BOTTOM) / 3); + let arrowWidth = arrowHeight * 2; + + let childBox = new Clutter.ActorBox(); + for (let i = 0; i < this._items.length; i++) { + let itemBox = this._items[i].allocation; + childBox.x1 = contentBox.x1 + Math.floor(itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2); + childBox.x2 = childBox.x1 + arrowWidth; + childBox.y1 = contentBox.y1 + itemBox.y2 + arrowHeight; + childBox.y2 = childBox.y1 + arrowHeight; + if (this._arrows[i]) + this._arrows[i].allocate(childBox); + } + } + + _onItemMotion(item) { + if (item === this._items[this._highlighted] || + item === this._items[this._delayedHighlighted]) + return Clutter.EVENT_PROPAGATE; + + const index = this._items.indexOf(item); + + if (this._mouseTimeOutId !== 0) { + GLib.source_remove(this._mouseTimeOutId); + this._delayedHighlighted = -1; + this._mouseTimeOutId = 0; + } + + if (this._altTabPopup && this._altTabPopup.thumbnailsVisible) { + this._delayedHighlighted = index; + this._mouseTimeOutId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + AltTab.APP_ICON_HOVER_TIMEOUT, + () => { + this._enterItem(index); + this._delayedHighlighted = -1; + this._mouseTimeOutId = 0; + return GLib.SOURCE_REMOVE; + }); + } else { + this._itemEntered(index); + } + + return Clutter.EVENT_PROPAGATE; + } + + _enterItem(index) { + let [x, y] = global.get_pointer(); + let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y); + if (this._items[index] && this._items[index].contains(pickedActor)) + this._itemEntered(index); + } + + highlight(n, justOutline) { + if (this.icons[this._highlighted]) { + if (this._arrows[this._highlighted]) { + if (this.icons[this._highlighted].cachedWindows.length === 1) + this._arrows[this._highlighted].hide(); + else + this._arrows[this._highlighted].remove_style_pseudo_class('highlighted'); + } + } + + let previous = this._items[this._highlighted]; + if (previous && previous.originalStyle) { + previous.set_style(previous.originalStyle); + } + + let item = this._items[n]; + if (item && item.colorPalette && item.colorPalette.baseRgb) { + let style = item.originalStyle || ''; + + // Read directly from the settings object passed to AppSwitcher + const keys = this._extensionSettings.list_keys(); + let shadeLevel = keys.includes('hover-shade-level') ? this._extensionSettings.get_double('hover-shade-level') : 0.2; + let borderWidth = keys.includes('hover-border-width') ? this._extensionSettings.get_int('hover-border-width') : 2; + let glowSize = keys.includes('hover-glow-size') ? this._extensionSettings.get_int('hover-glow-size') : 15; + let opacity = keys.includes('hover-opacity') ? this._extensionSettings.get_double('hover-opacity') : 0.8; + let iconBorderRadius = this._iconBorderRadius; + + let r = Math.round(Math.min(Math.max(item.colorPalette.baseRgb.r * (1 + shadeLevel), 0), 255)); + let g = Math.round(Math.min(Math.max(item.colorPalette.baseRgb.g * (1 + shadeLevel), 0), 255)); + let b = Math.round(Math.min(Math.max(item.colorPalette.baseRgb.b * (1 + shadeLevel), 0), 255)); + let hoverColor = `rgba(${r}, ${g}, ${b}, ${opacity})`; + + item.set_style(style + + ' box-shadow: inset 0 0 ' + glowSize + 'px ' + hoverColor + ', 0 0 0 ' + borderWidth + 'px ' + hoverColor + ' !important; ' + + ' border: 3px solid transparent !important; ' + + ' border-radius: ' + iconBorderRadius + 'px !important;'); + } + + highlight2.call(this, n, justOutline); + this._curApp = n; + + if (this._highlighted !== -1 && this.icons[this._highlighted] && this._arrows[this._highlighted]) { + if (justOutline && this.icons[this._highlighted].cachedWindows.length === 1) + this._arrows[this._highlighted].show(); + else + this._arrows[this._highlighted].add_style_pseudo_class('highlighted'); + } + } + + _addIcon(appIcon) { + this.icons.push(appIcon); + let item = this.addItem(appIcon, appIcon.label); + + if (appIcon.label) { + appIcon.label.set_style('text-shadow: 0px 1px 3px rgba(0, 0, 0, 0.7); font-weight: bold;'); + } + + try { + item.colorPalette = new Utils.DominantColorExtractor(appIcon.app)._getColorPalette(); + } catch (e) { + item.colorPalette = null; + } + + const iconBorderRadius = this._iconBorderRadius; + + if (item.colorPalette == null) { + item.colorPalette = { + original: '#000000', + lighter: '#666666', + darker: '#000000', + baseRgb: { r: 0, g: 0, b: 0 } + } + } + let hex = item.colorPalette.original; + let rgb = Utils.ColorUtils._hexToRgb(hex); + // Force background-color and remove background-image (gradients) using !important + item.originalStyle = 'background-color: rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ', 0.6) !important; background-image: none !important; border: 3px solid transparent; border-radius: ' + iconBorderRadius + 'px !important;'; + item.set_style(item.originalStyle); + + appIcon.app.connectObject('notify::state', app => { + if (app.state !== Shell.AppState.RUNNING) + this._removeIcon(app); + }, this); + + let arrow = new St.DrawingArea({ style_class: 'switcher-arrow' }); + arrow.connect('repaint', () => SwitcherPopup.drawArrow(arrow, St.Side.BOTTOM)); + this.add_child(arrow); + this._arrows.push(arrow); + + if (appIcon.cachedWindows.length === 1) + arrow.hide(); + else + item.add_accessible_state(Atk.StateType.EXPANDABLE); + } + + _removeIcon(app) { + let index = this.icons.findIndex(icon => { + return icon.app === app; + }); + if (index === -1) + return; + + if (this._arrows[index]) { + this._arrows[index].destroy(); + this._arrows.splice(index, 1); + } + + this.icons.splice(index, 1); + this.removeItem(index); + } + }); diff --git a/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/locale/en/LC_MESSAGES/unity-window-switcher.mo b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/locale/en/LC_MESSAGES/unity-window-switcher.mo Binary files differnew file mode 100644 index 0000000..5f0d96e --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/locale/en/LC_MESSAGES/unity-window-switcher.mo diff --git a/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/locale/hu/LC_MESSAGES/unity-window-switcher.mo b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/locale/hu/LC_MESSAGES/unity-window-switcher.mo Binary files differnew file mode 100644 index 0000000..9e8b840 --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/locale/hu/LC_MESSAGES/unity-window-switcher.mo diff --git a/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/metadata.json b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/metadata.json new file mode 100644 index 0000000..1d1638c --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/metadata.json @@ -0,0 +1,14 @@ +{ + "name": "Unity-like App Switcher (Reforged)", + "description": "A modern, AI-reforged version of the Unity-like AppSwitcher for GNOME 48+. Bigger, more colorful, and highly customizable.", + "original-authors": "gonza", + "uuid": "unity-like-appswitcher-reforged@gabeszm", + "gettext-domain": "unity-window-switcher", + "url": "https://github.com/gonzaarcr/unity-like-switcher-gnome-ext", + "settings-schema": "org.gnome.shell.extensions.unity-window-switcher-reforged", + "shell-version": [ + "48", + "49", + "50" + ] +} diff --git a/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/prefs.js b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/prefs.js new file mode 100644 index 0000000..92949e6 --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/prefs.js @@ -0,0 +1,256 @@ +import Adw from 'gi://Adw'; +import GObject from "gi://GObject"; +import Gio from "gi://Gio"; +import Gtk from "gi://Gtk"; +import Gdk from "gi://Gdk"; + +import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + + +class PrefsWidget { + + constructor(schema) { + this._updating = false; + this._buildable = new Gtk.Builder(); + this._buildable.add_from_file( + Gio.File.new_for_uri(import.meta.url).get_parent().get_path() + '/settings.ui' + ); + + let prefsWidget = this._getWidget('prefs_widget'); + if (!prefsWidget) { + console.error('UnityLikeAppSwitcher: Main widget not found'); + } + + this._settings = schema; + this._bindBooleans(); + this._bindNumbers(); + this._bindStrings(); + + let resetButton = this._getWidget('reset_button'); + if (resetButton) { + resetButton.connect('clicked', () => { + this._resetAll(); + }); + } + + this._settings.connect( + 'changed::first-change-window', + this._firstChangeWindowChanged.bind(this) + ); + this._firstChangeWindowChanged(); + } + + _resetAll() { + this._updating = true; + try { + this._getNumbers().forEach(setting => { + this._settings.reset(setting); + // Trigger UI update manually for numbers since they use manual binding + this._updateNumberWidget(setting); + }); + + this._getBooleans().forEach(setting => { + this._settings.reset(setting); + }); + + this._getStrings().forEach(setting => { + this._settings.reset(setting); + this._updateStringWidget(setting); + }); + + Gio.Settings.sync(); + } finally { + this._updating = false; + } + } + + _updateNumberWidget(setting) { + if (this._updating) return; + let widget = this._getWidget(setting); + if (!widget || !widget.set_value) { + return; + } + if (!this._settings.list_keys().includes(setting)) { + // Fallback values if key is missing + let defaultValues = { + 'hover-shade-level': 0.2, + 'hover-opacity': 0.8, + 'hover-border-width': 2, + 'hover-glow-size': 15, + 'icon-spacing': 10, + 'container-padding': 24, + 'border-size': 2, + 'background-border-radius': 24, + 'icon-border-radius': 16 + }; + widget.set_value(defaultValues[setting] || 0); + return; + } + let isDouble = (setting === 'hover-shade-level' || setting === 'hover-opacity'); + if (isDouble) { + widget.set_value(this._settings.get_double(setting)); + } else { + widget.set_value(this._settings.get_int(setting)); + } + } + + _updateStringWidget(setting) { + if (this._updating) return; + let widget = this._getWidget(setting); + if (!widget) return; + let colorStr = ''; + if (!this._settings.list_keys().includes(setting)) { + let defaultValues = { + 'background-color': 'rgba(0, 0, 0, 0.5)', + 'border-color': 'rgba(255, 255, 255, 0.2)' + }; + colorStr = defaultValues[setting] || ''; + } else { + colorStr = this._settings.get_string(setting); + } + + if (widget instanceof Gtk.ColorDialogButton) { + let rgba = new Gdk.RGBA(); + if (rgba.parse(colorStr)) { + this._updating = true; + widget.set_rgba(rgba); + this._updating = false; + } + } else if (widget.set_text) { + this._updating = true; + widget.set_text(colorStr); + this._updating = false; + } + } + + _getWidget(name) { + let wname = name.replace(/-/g, '_'); + return this._buildable.get_object(wname); + } + + _getBooleans() { + return [ + 'first-change-window' + ]; + } + + _getNumbers() { + return [ + 'hover-shade-level', + 'hover-border-width', + 'hover-glow-size', + 'hover-opacity', + 'icon-spacing', + 'container-padding', + 'border-size', + 'background-border-radius', + 'icon-border-radius' + ]; + } + + _getStrings() { + return [ + 'background-color', + 'border-color' + ]; + } + + _bindBoolean(setting) { + let widget = this._getWidget(setting); + if (!widget) return; + this._settings.bind(setting, widget, 'active', Gio.SettingsBindFlags.DEFAULT); + } + + _bindBooleans() { + this._getBooleans().forEach(this._bindBoolean, this); + } + + _bindString(setting) { + let widget = this._getWidget(setting); + if (!widget) return; + this._updateStringWidget(setting); + + if (widget instanceof Gtk.ColorDialogButton) { + widget.connect('notify::rgba', (w) => { + if (this._updating) return; + if (!this._settings.list_keys().includes(setting)) return; + let rgba = w.get_rgba(); + let val = rgba.to_string(); // CSS format + let current = this._settings.get_string(setting); + if (current !== val) { + this._settings.set_string(setting, val); + } + }); + } else { + widget.connect('changed', (w) => { + if (this._updating) return; + if (!this._settings.list_keys().includes(setting)) return; + let val = w.get_text(); + let current = this._settings.get_string(setting); + if (current !== val) { + this._settings.set_string(setting, val); + } + }); + } + + this._settings.connect(`changed::${setting}`, () => { + if (this._updating) return; + this._updateStringWidget(setting); + }); + } + + _bindStrings() { + this._getStrings().forEach(this._bindString, this); + } + + _bindNumber(setting) { + let widget = this._getWidget(setting); + if (!widget) return; + let isDouble = (setting === 'hover-shade-level' || setting === 'hover-opacity'); + + // Érték betöltése indításkor + this._updateNumberWidget(setting); + + // Automatikus mentés változáskor + widget.connect('notify::value', (w) => { + if (this._updating) return; + if (!this._settings.list_keys().includes(setting)) return; + let val = w.get_value(); + if (isDouble) { + // Only update if significantly different to avoid feedback loops + let current = this._settings.get_double(setting); + if (Math.abs(current - val) > 0.001) { + this._settings.set_double(setting, val); + } + } else { + let current = this._settings.get_int(setting); + if (current !== Math.round(val)) { + this._settings.set_int(setting, Math.round(val)); + } + } + }); + + // Update widget if setting changes externally (e.g. reset) + this._settings.connect(`changed::${setting}`, () => { + if (this._updating) return; + this._updateNumberWidget(setting); + }); + } + + _bindNumbers() { + this._getNumbers().forEach(this._bindNumber, this); + } + + _firstChangeWindowChanged() { + this._settings.get_boolean('first-change-window'); + } +} + +export default class UnityLikeAppSwitcherPreferences extends ExtensionPreferences { + fillPreferencesWindow (window) { + this.initTranslations("unity-window-switcher"); + window._settings = this.getSettings(); + const widget = new PrefsWidget(window._settings); + window.add(widget._getWidget('prefs_widget')); + } +} diff --git a/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/schemas/gschemas.compiled b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/schemas/gschemas.compiled Binary files differnew file mode 100644 index 0000000..a6544ab --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/schemas/gschemas.compiled diff --git a/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/schemas/org.gnome.shell.extensions.unity-window-switcher-reforged.gschema.xml b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/schemas/org.gnome.shell.extensions.unity-window-switcher-reforged.gschema.xml new file mode 100644 index 0000000..e616c9f --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/schemas/org.gnome.shell.extensions.unity-window-switcher-reforged.gschema.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<schemalist gettext-domain="unity-window-switcher"> + <schema id="org.gnome.shell.extensions.unity-window-switcher-reforged" path="/org/gnome/shell/extensions/unity-window-switcher-reforged/"> + <key name="first-change-window" type="b"> + <default>true</default> + <summary>Focus the second window when switching</summary> + <description>When the switcher is opened, the second window is focused by default, similar to the original Unity behavior.</description> + </key> + <key name="hover-shade-level" type="d"> + <default>1.0</default> + <summary>Hover Shade Level</summary> + <description>The brightness multiplier for the hover glow (e.g., 0.1 means 10% brighter).</description> + </key> + <key name="hover-border-width" type="i"> + <default>2</default> + <summary>Hover Border Width</summary> + <description>The thickness of the border around the hovered item in pixels.</description> + </key> + <key name="hover-glow-size" type="i"> + <range min="0" max="30"/> + <default>8</default> + <summary>Hover Glow Size</summary> + <description>The size of the inner glow (box-shadow) for the hovered item.</description> + </key> + <key name="hover-opacity" type="d"> + <default>0.9</default> + <summary>Hover Opacity</summary> + <description>The opacity of the hover highlight (0.0 to 1.0).</description> + </key> + <key name="icon-spacing" type="i"> + <default>10</default> + <summary>Icon Spacing</summary> + <description>The distance between icons in pixels.</description> + </key> + <key name="container-padding" type="i"> + <default>10</default> + <summary>Container Padding</summary> + <description>The padding around the icons inside the switcher.</description> + </key> + <key name="background-color" type="s"> + <default>'rgb(24,24,26)'</default> + <summary>Background Color</summary> + <description>The background color of the switcher (CSS format).</description> + </key> + <key name="border-size" type="i"> + <default>0</default> + <summary>Border Size</summary> + <description>The thickness of the switcher border.</description> + </key> + <key name="border-color" type="s"> + <default>'rgba(255, 255, 255, 0.2)'</default> + <summary>Border Color</summary> + <description>The color of the switcher border (CSS format).</description> + </key> + <key name="background-border-radius" type="i"> + <default>24</default> + <summary>Background Border Radius</summary> + <description>The border radius of the main switcher container in pixels.</description> + </key> + <key name="icon-border-radius" type="i"> + <default>16</default> + <summary>Icon Border Radius</summary> + <description>The border radius of the application icons in pixels.</description> + </key> + </schema> +</schemalist> diff --git a/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/settings.ui b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/settings.ui new file mode 100644 index 0000000..ac10116 --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/settings.ui @@ -0,0 +1,215 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface domain="unity-window-switcher"> + <requires lib="gtk" version="4.0"/> + <requires lib="adw" version="1.0"/> + <object class="AdwPreferencesPage" id="prefs_widget"> + <property name="width_request">500</property> + <property name="visible">True</property> + + <child> + <object class="AdwPreferencesGroup"> + <property name="title" translatable="yes">Behavior</property> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">First tab changes windows (or apps)</property> + <child> + <object class="GtkSwitch" id="first_change_window"> + <property name="valign">center</property> + <property name="can_focus">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwPreferencesGroup"> + <property name="title" translatable="yes">Panel Appearance</property> + <child> + <object class="AdwActionRow" id="background_color_row"> + <property name="title" translatable="yes">Background Color</property> + <child type="suffix"> + <object class="GtkColorDialogButton" id="background_color"> + <property name="valign">center</property> + <property name="dialog"> + <object class="GtkColorDialog"> + <property name="with_alpha">True</property> + </object> + </property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwSpinRow" id="background_border_radius"> + <property name="title" translatable="yes">Background Border Radius</property> + <property name="subtitle" translatable="yes">The corner radius of the main switcher window</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">0</property> + <property name="upper">100</property> + <property name="step_increment">1</property> + <property name="value">24</property> + </object> + </property> + </object> + </child> + <child> + <object class="AdwSpinRow" id="container_padding"> + <property name="title" translatable="yes">Container Padding</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">0</property> + <property name="upper">100</property> + <property name="step_increment">1</property> + <property name="value">24</property> + </object> + </property> + </object> + </child> + <child> + <object class="AdwSpinRow" id="border_size"> + <property name="title" translatable="yes">Border Size</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">0</property> + <property name="upper">20</property> + <property name="step_increment">1</property> + <property name="value">2</property> + </object> + </property> + </object> + </child> + <child> + <object class="AdwActionRow" id="border_color_row"> + <property name="title" translatable="yes">Border Color</property> + <child type="suffix"> + <object class="GtkColorDialogButton" id="border_color"> + <property name="valign">center</property> + <property name="dialog"> + <object class="GtkColorDialog"> + <property name="with_alpha">True</property> + </object> + </property> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwPreferencesGroup"> + <property name="title" translatable="yes">Icons</property> + <child> + <object class="AdwSpinRow" id="icon_spacing"> + <property name="title" translatable="yes">Icon Spacing</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">0</property> + <property name="upper">100</property> + <property name="step_increment">1</property> + <property name="value">10</property> + </object> + </property> + </object> + </child> + <child> + <object class="AdwSpinRow" id="icon_border_radius"> + <property name="title" translatable="yes">Icon Border Radius</property> + <property name="subtitle" translatable="yes">The corner radius of the application icons</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">0</property> + <property name="upper">100</property> + <property name="step_increment">1</property> + <property name="value">16</property> + </object> + </property> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwPreferencesGroup"> + <property name="title" translatable="yes">Hover Effect</property> + <child> + <object class="AdwSpinRow" id="hover_shade_level"> + <property name="title" translatable="yes">Brightness level</property> + <property name="subtitle" translatable="yes">Darker (< 0) or lighter (> 0) hover effect (-1.0 to 1.0)</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">-1</property> + <property name="upper">1</property> + <property name="step_increment">0.05</property> + <property name="page_increment">0.1</property> + <property name="value">0.5</property> + </object> + </property> + <property name="digits">2</property> + </object> + </child> + <child> + <object class="AdwSpinRow" id="hover_opacity"> + <property name="title" translatable="yes">Opacity</property> + <property name="subtitle" translatable="yes">Transparency of the hover effect (0.0 to 1.0)</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">0</property> + <property name="upper">1</property> + <property name="step_increment">0.05</property> + <property name="page_increment">0.1</property> + <property name="value">0.8</property> + </object> + </property> + <property name="digits">2</property> + </object> + </child> + <child> + <object class="AdwSpinRow" id="hover_border_width"> + <property name="title" translatable="yes">Border width</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">0</property> + <property name="upper">20</property> + <property name="step_increment">1</property> + <property name="value">3</property> + </object> + </property> + </object> + </child> + <child> + <object class="AdwSpinRow" id="hover_glow_size"> + <property name="title" translatable="yes">Glow size</property> + <property name="adjustment"> + <object class="GtkAdjustment"> + <property name="lower">0</property> + <property name="upper">30</property> + <property name="step_increment">1</property> + <property name="value">20</property> + </object> + </property> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkButton" id="reset_button"> + <property name="label" translatable="yes">Reset</property> + <property name="halign">center</property> + <property name="margin_top">20</property> + <property name="margin_bottom">20</property> + <style> + <class name="destructive-action"/> + </style> + </object> + </child> + </object> + </child> + </object> +</interface> diff --git a/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/utils.js b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/utils.js new file mode 100644 index 0000000..898b047 --- /dev/null +++ b/raveos-theme/gnome/theme-data/extensions/installed/unity-like-appswitcher-reforged@gabeszm/utils.js @@ -0,0 +1,346 @@ +import Gdk from "gi://Gdk"; +import GdkPixbuf from "gi://GdkPixbuf"; +import Gio from "gi://Gio"; +import Gtk from "gi://Gtk"; +import St from "gi://St"; +import GLib from "gi://GLib"; + +let iconCacheMap = new Map(); +const MAX_CACHED_ITEMS = 1000; +const BATCH_SIZE_TO_DELETE = 50; +const DOMINANT_COLOR_ICON_SIZE = 64; + +function debugLog(msg) { + try { + let f = Gio.File.new_for_path('/tmp/color_log.txt'); + let out = f.append_to(Gio.FileCreateFlags.NONE, null); + out.write_all(new Date().toISOString() + " - " + msg + "\n", null); + out.close(null); + } catch (e) { } +} + +function findFallbackIconPath(iconName) { + if (!iconName) return null; + if (GLib.file_test(iconName, GLib.FileTest.EXISTS)) return iconName; + + const sizes = ['128x128', '256x256', '512x512', '64x64', 'scalable', '48x48']; + + let currentTheme = 'Adwaita'; + try { + const settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' }); + if (settings.get_string('icon-theme')) { + currentTheme = settings.get_string('icon-theme'); + } + } catch (e) { + debugLog("Failed to get active icon theme setting: " + e); + } + + // Tegyük a hicolort az első helyre, a jelenleg aktív témát a másodikra, aztán a többi népszerű. + // A Set-tel kiszűrjük az esetleges ismétlődéseket. + const themes = [...new Set(['hicolor', currentTheme, 'locolor', 'Yaru', 'Pop', 'Adwaita', 'MoreWaita', 'Papirus', 'Tela'])]; + const bases = [ + GLib.get_home_dir() + '/.local/share/icons', + GLib.get_home_dir() + '/.local/share/flatpak/exports/share/icons', + '/var/lib/flatpak/exports/share/icons', + '/var/lib/snapd/desktop/icons', + '/usr/share/icons', + '/usr/local/share/icons' + ]; + const formats = ['.svg', '.png', '.xpm']; + + for (const base of bases) { + for (const theme of themes) { + for (const size of sizes) { + for (const format of formats) { + let path = `${base}/${theme}/${size}/apps/${iconName}${format}`; + if (GLib.file_test(path, GLib.FileTest.EXISTS)) return path; + } + } + } + } + + // Check pixmaps + for (const format of formats) { + let path = `/usr/share/pixmaps/${iconName}${format}`; + if (GLib.file_test(path, GLib.FileTest.EXISTS)) return path; + } + + return null; +} + +export class DominantColorExtractor { + constructor(app) { + this._app = app; + } + + _getIconPixBuf() { + debugLog("--- START Extracting for app: " + (this._app ? this._app.get_id() : "undefined") + " ---"); + if (!this._app) { + debugLog("Error: _app is null or undefined"); + return null; + } + + const gicon = this._app.get_icon(); + if (!gicon) { + debugLog("Fail: get_icon() returned null"); + return null; + } + + debugLog("Got gicon of type: " + gicon.constructor.name); + + // Try to load via Gio.FileIcon directly + if (gicon instanceof Gio.FileIcon) { + try { + debugLog("gicon is Gio.FileIcon. Getting file path..."); + const path = gicon.get_file().get_path(); + debugLog("File path: " + path); + if (path) { + return GdkPixbuf.Pixbuf.new_from_file(path); + } + } catch (e) { + debugLog("Exception in Gio.FileIcon lookup: " + e); + } + } + + // Try fallback manual lookup + if (gicon instanceof Gio.ThemedIcon) { + try { + debugLog("Attempting manual fallback search for ThemedIcon..."); + const names = gicon.get_names(); + for (const name of names) { + let path = findFallbackIconPath(name); + if (path) { + debugLog("Manual fallback found file: " + path); + return GdkPixbuf.Pixbuf.new_from_file(path); + } + } + } catch (e) { + debugLog("Exception in manual fallback: " + e); + } + } + + // Try St.TextureCache as a last resort + try { + debugLog("Attempting St.TextureCache lookup..."); + let textureCache = St.TextureCache.get_default(); + let themeContext = St.ThemeContext.get_for_stage(global.stage); + let iconSize = 64 * themeContext.scale_factor; + + let iconTexture = textureCache.load_gicon(null, gicon, iconSize); + if (iconTexture) { + debugLog("Found texture in cache, but cannot easily convert to Pixbuf in this context. Fallback to default."); + } + } catch (e) { + debugLog("St.TextureCache lookup failed: " + e); + } + + debugLog("--- FAIL: Exhausted all methods to get icon pixbuf ---"); + return null; + } + + _getColorPalette() { + debugLog("--- START _getColorPalette ---"); + const appId = this._app ? this._app.get_id() : null; + debugLog("App ID: " + appId); + + if (appId && iconCacheMap.has(appId)) { + debugLog("Returning cached color for: " + appId); + return iconCacheMap.get(appId); + } + + debugLog("Cache miss, calling _getIconPixBuf()"); + const pixBuf = this._getIconPixBuf(); + if (!pixBuf) { + debugLog("FAIL: pixBuf is null, cannot extract color."); + return null; + } + + try { + const width = pixBuf.get_width(); + const height = pixBuf.get_height(); + const rowstride = pixBuf.get_rowstride(); + const nChannels = pixBuf.get_n_channels(); + const hasAlpha = pixBuf.get_has_alpha(); + + debugLog(`PixBuf info: ${width}x${height}, rowstride: ${rowstride}, channels: ${nChannels}, alpha: ${hasAlpha}`); + + const pixels = pixBuf.get_pixels(); + if (!pixels) { + debugLog("FAIL: pixBuf.get_pixels() returned null or undefined"); + return null; + } + + let total = 0, rTotal = 0, gTotal = 0, bTotal = 0; + const step = Math.max(1, Math.floor(width / 32)); + debugLog("Calculated step for sampling: " + step); + + let sampledCount = 0; + for (let y = 0; y < height; y += step) { + for (let x = 0; x < width; x += step) { + const offset = y * rowstride + x * nChannels; + const r = pixels[offset]; + const g = pixels[offset + 1]; + const b = pixels[offset + 2]; + const a = hasAlpha ? pixels[offset + 3] : 255; + + if (a < 128) continue; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const saturation = (max - min) / (max || 1); + const relevance = a * (saturation * saturation + 0.1); + + rTotal += r * relevance; + gTotal += g * relevance; + bTotal += b * relevance; + total += relevance; + sampledCount++; + } + } + + debugLog(`Sampling complete. Sampled pixels: ${sampledCount}. Total accumulator: ${total}`); + + if (total === 0) { + debugLog("FAIL: Valid pixel total is 0. Returning null."); + return null; + } + + let r = rTotal / total; + let g = gTotal / total; + let b = bTotal / total; + debugLog(`Averaged RGB: Math.round(${r}), Math.round(${g}), Math.round(${b})`); + + let hsv = ColorUtils.RGBtoHSV(r, g, b); + debugLog(`HSV before normalize: h:${hsv.h.toFixed(2)}, s:${hsv.s.toFixed(2)}, v:${hsv.v.toFixed(2)}`); + + if (hsv.s < 0.2) hsv.s = 0.35; + hsv.v = Math.max(0.6, Math.min(hsv.v, 0.9)); + + debugLog(`HSV after normalize: h:${hsv.h.toFixed(2)}, s:${hsv.s.toFixed(2)}, v:${hsv.v.toFixed(2)}`); + + const rgb = ColorUtils.HSVtoRGB(hsv.h, hsv.s, hsv.v); + + const backgroundColor = { + lighter: ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0.5), + original: ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, 0), + darker: ColorUtils.ColorLuminance(rgb.r, rgb.g, rgb.b, -0.5), + baseRgb: rgb + }; + + debugLog(`Generated Palette: original=${backgroundColor.original}, lighter=${backgroundColor.lighter}, darker=${backgroundColor.darker}`); + + if (iconCacheMap.size >= MAX_CACHED_ITEMS) { + debugLog("Cache full, clearing batch."); + const keys = Array.from(iconCacheMap.keys()); + for (let i = 0; i < BATCH_SIZE_TO_DELETE; i++) { + iconCacheMap.delete(keys[i]); + } + } + + if (appId) { + iconCacheMap.set(appId, backgroundColor); + debugLog("Added to cache for appId: " + appId); + } + + debugLog("--- SUCCESS: Color extraction finished ---"); + return backgroundColor; + + } catch (e) { + debugLog("Exception during pixel processing: " + e); + return null; + } + } +} + +export class ColorUtils { + static ColorLuminance(r, g, b, dlum) { + let rgbString = '#'; + rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(r * (1 + dlum), 0), 255)), 2); + rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(g * (1 + dlum), 0), 255)), 2); + rgbString += ColorUtils._decimalToHex(Math.round(Math.min(Math.max(b * (1 + dlum), 0), 255)), 2); + return rgbString; + } + + static _decimalToHex(d, padding) { + let hex = d.toString(16); + while (hex.length < padding) + hex = '0' + hex; + return hex; + } + + static _hexToRgb(h) { + return { + r: parseInt(h.substr(1, 2), 16), + g: parseInt(h.substr(3, 2), 16), + b: parseInt(h.substr(5, 2), 16) + } + } + + static HSVtoRGB(h, s, v) { + if (arguments.length === 1) { + s = h.s; + v = h.v; + h = h.h; + } + + let r, g, b; + let c = v * s; + let h1 = h * 6; + let x = c * (1 - Math.abs(h1 % 2 - 1)); + let m = v - c; + + if (h1 <= 1) + r = c + m, g = x + m, b = m; + else if (h1 <= 2) + r = x + m, g = c + m, b = m; + else if (h1 <= 3) + r = m, g = c + m, b = x + m; + else if (h1 <= 4) + r = m, g = x + m, b = c + m; + else if (h1 <= 5) + r = x + m, g = m, b = c + m; + else + r = c + m, g = m, b = x + m; + + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; + } + + static RGBtoHSV(r, g, b) { + if (arguments.length === 1) { + r = r.r; + g = r.g; + b = r.b; + } + + let h, s, v; + let M = Math.max(r, g, b); + let m = Math.min(r, g, b); + let c = M - m; + + if (c == 0) + h = 0; + else if (M == r) + h = ((g - b) / c) % 6; + else if (M == g) + h = (b - r) / c + 2; + else + h = (r - g) / c + 4; + + h = h / 6; + v = M / 255; + if (M !== 0) + s = c / M; + else + s = 0; + + return { + h: h, + s: s, + v: v + }; + } +} |