Compare commits

..

3 Commits

Author SHA1 Message Date
Florian Müllner
bf5230ef6c Bump version to 3.22.2
Update NEWS.
2016-11-10 16:45:43 +01:00
YunQiang Su
1a0601dda8 Fix zh_CN translation: cycle screenshot sizes 2016-10-18 17:54:11 +08:00
YunQiang Su
9117b2266c Update zh_CN translation 2016-10-18 17:40:24 +08:00
172 changed files with 16504 additions and 17252 deletions

View File

@@ -1,3 +0,0 @@
extends:
- ./lint/eslintrc-gjs.yml
- ./lint/eslintrc-shell.yml

View File

@@ -1,177 +0,0 @@
include:
- remote: 'https://gitlab.freedesktop.org/freedesktop/ci-templates/-/raw/bbe5232986c9b98eb1efe62484e07216f7d1a4df/templates/fedora.yml'
- remote: "https://gitlab.freedesktop.org/freedesktop/ci-templates/-/raw/6f86b8bcb0cd5168c32779c4fea9a893c4a0c046/templates/ci-fairy.yml"
image: registry.gitlab.gnome.org/gnome/gnome-shell/fedora/34:2021-08-12.0
stages:
- pre_review
- prepare
- review
- build
- deploy
default:
# Cancel jobs if newer commits are pushed to the branch
interruptible: true
# Auto-retry jobs in case of infra failures
retry:
max: 1
when:
- 'runner_system_failure'
- 'stuck_or_timeout_failure'
- 'scheduler_failure'
- 'api_failure'
variables:
FDO_UPSTREAM_REPO: GNOME/gnome-shell-extensions
LINT_LOG: "eslint-report.xml"
JS_LOG: "js-report.txt"
workflow:
rules:
- if: '$CI_MERGE_REQUEST_IID'
- if: '$CI_COMMIT_TAG'
- if: '$CI_COMMIT_BRANCH'
.pipeline_guard: &pipeline_guard
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_TAG'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_COMMIT_BRANCH =~ /^gnome-[0-9-]+$/'
- when: 'manual'
.gnome-shell-extensions.fedora:34:
variables:
FDO_DISTRIBUTION_VERSION: 34
FDO_DISTRIBUTION_TAG: '2021-08-31.0'
FDO_DISTRIBUTION_PACKAGES: >
meson git gettext sassc
.prereview_req: &prereview_req
needs:
- check_commit_log
- check-merge-request
check_commit_log:
extends:
- .fdo.ci-fairy
stage: pre_review
script:
- if [[ x"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" != "x" ]] ;
then
ci-fairy check-commits --junit-xml=commit-message-junit-report.xml ;
else
echo "Not a merge request" ;
fi
<<: *pipeline_guard
artifacts:
expire_in: 1 week
paths:
- commit-message-junit-report.xml
reports:
junit: commit-message-junit-report.xml
check-merge-request:
extends:
- .fdo.ci-fairy
stage: pre_review
script:
- if [[ x"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" != "x" ]] ;
then
ci-fairy check-merge-request --require-allow-collaboration --junit-xml=check-merge-request-report.xml ;
else
echo "Not a merge request" ;
fi
<<: *pipeline_guard
artifacts:
expire_in: 1 week
paths:
- check-merge-request-report.xml
reports:
junit: check-merge-request-report.xml
build-fedora-container:
extends:
- .fdo.container-build@fedora@x86_64
- .gnome-shell-extensions.fedora:34
stage: prepare
<<: *prereview_req
js_check:
stage: review
<<: *prereview_req
script:
- find extensions -name '*.js' -exec js78 -c '{}' ';' 2>&1 | tee $JS_LOG
- (! grep -q . $JS_LOG)
artifacts:
paths:
- ${JS_LOG}
when: on_failure
eslint:
stage: review
<<: *prereview_req
script:
- eslint -o $LINT_LOG -f junit --resolve-plugins-relative-to $(npm root -g) extensions
artifacts:
paths:
- ${LINT_LOG}
reports:
junit: ${LINT_LOG}
build-bundles:
stage: build
<<: *prereview_req
script:
- ./export-zips.sh
artifacts:
name: 'Extension bundles'
expose_as: 'Get Extension bundles here'
paths:
- zip-files/
fedora-build:
extends:
- .fdo.distribution-image@fedora
- .gnome-shell-extensions.fedora:34
stage: build
needs:
- build-fedora-container
before_script:
- git submodule update --init
script:
- meson setup build --werror -Dextension_set=all -Dclassic_mode=true
- meson compile -C build
- meson test -C build
- meson install -C build
artifacts:
paths:
- build
fedora-dist:
extends:
- .fdo.distribution-image@fedora
- .gnome-shell-extensions.fedora:34
stage: deploy
needs:
- fedora-build
before_script:
- git submodule update --init
script:
- meson dist -C build
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- "**/meson.build"
- meson/*
fedora-dist-tarball:
extends: fedora-dist
artifacts:
expose_as: 'Get tarball here'
paths:
- build/meson-dist/$CI_PROJECT_NAME-$CI_COMMIT_TAG.tar.xz
rules:
- if: '$CI_COMMIT_TAG'

View File

@@ -1,13 +0,0 @@
patterns:
deny:
- regex: '^$CI_MERGE_REQUEST_PROJECT_URL/(-/)?merge_requests/$CI_MERGE_REQUEST_IID$'
message: Commit message must not contain a link to its own merge request
- regex: '^extensions/'
message: Commit message subject should not be prefixed with 'extensions/', use the extension name instead
where: subject
- regex: '^[^:]+: [a-z]'
message: "Commit message subject should be properly Capitalized. E.g. 'window: Marginalize extradicity'"
where: subject
- regex: '^\S*\.js:'
message: Commit message subject prefix should not include .js
where: subject

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "data/gnome-shell-sass"]
path = data/gnome-shell-sass
url = https://gitlab.gnome.org/GNOME/gnome-shell-sass.git
url = https://git.gnome.org/browse/gnome-shell-sass/

31
HACKING Normal file
View File

@@ -0,0 +1,31 @@
--- Creating a New Extension ---
To create a new extension, add a subdirectory in extensions.
Then create a Makefile.am like the one in example, replacing
the EXTENSION_ID with the basename of your extension, which
must match the UUID in metadata.json.
If you need additional files, add them to EXTENSION_EXTRA.
Then modify extensions/Makefile.am and configure.ac. It should
be pretty self-explanatory.
Don't forget to add any translatable file to po/POTFILES.in, and
then you're done.
The Gettext domain you should choose is gnome-shell-extensions,
not gnome-shell, unless you're sure there is the string you
need in gnome-shell.
--- Coding Style ---
Generally, we follow GJS coding style (you can find it at
http://git.gnome.org/browse/gjs/tree/doc/Style_Guide.txt), which
in short is: indent 4 spaces, no tabs, space after comma, no space
after function call.
The Emacs mode line for this
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
Imports should be at the top, in two groups, one for standard
imports (like imports.lang or imports.dbus) and introspection,
the other for Shell API. Within the same group, put everything
in alphabetic order.

View File

@@ -1,31 +0,0 @@
## Creating a New Extension
To create a new extension, add a subdirectory in extensions. Then create
a meson.build from the provided [template](extensions/meson.build.template).
If you need additional sources, add them to extension_sources. Similarily add
GSettings schemas to extension_schemas and other files to extension_data.
Then modify the [toplevel Meson file](meson.build) to add the new
extension name in the appropriate set (that is one of classic_extensions,
default_extensions or all_extensions).
Don't forget to add any translatable file to po/POTFILES.in, and
then you're done.
The Gettext domain you should choose is gnome-shell-extensions,
not gnome-shell, unless you're sure there is the string you
need in gnome-shell.
## Coding Style
Generally, we follow [GJS coding style][coding-style], which in short is:
indent 4 spaces, no tabs, space after comma, no space after function call.
The Emacs mode line for this
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
Imports should be at the top, in two groups, one for standard
imports (like imports.lang or imports.dbus) and introspection,
the other for Shell API. Within the same group, put everything
in alphabetic order.
[coding-style]: https://gitlab.gnome.org/GNOME/gjs/blob/HEAD/doc/Style_Guide.md

52
Makefile.am Normal file
View File

@@ -0,0 +1,52 @@
ACLOCAL_AMFLAGS = -I m4 ${ACLOCAL_FLAGS}
DIST_SUBDIRS = data extensions po
SUBDIRS = extensions po
if CLASSIC_MODE
SUBDIRS += data
endif
EXTRA_DIST = lib/convenience.js
DISTCHECK_CONFIGURE_FLAGS = --enable-extensions=all
include include.mk
zip-file: all
-rm -fR $(builddir)/_build
-rm -fR $(builddir)/zip-files
$(MKDIR_P) $(builddir)/_build; \
$(MKDIR_P) $(builddir)/zip-files; \
$(MAKE) install DESTDIR="$(abs_builddir)/_build"; \
for i in $(ENABLED_EXTENSIONS); do \
mv "$(builddir)/_build$(topextensiondir)/$${i}$(extensionbase)" "$(builddir)/_build/"; \
cp -r "$(builddir)/_build$(datadir)/locale" "$(builddir)/_build/$${i}$(extensionbase)"; \
cp "$(srcdir)/COPYING" -t "$(builddir)/_build/$${i}$(extensionbase)"; \
cp "$(srcdir)/NEWS" -t "$(builddir)/_build/$${i}$(extensionbase)"; \
if [ -f "$(builddir)/_build$(datadir)/glib-2.0/schemas/$(gschemabase).$${i}.gschema.xml" ]; then \
$(MKDIR_P) "$(builddir)/_build/$${i}$(extensionbase)/schemas"; \
mv "$(builddir)/_build$(datadir)/glib-2.0/schemas/$(gschemabase).$${i}.gschema.xml" "$(builddir)/_build/$${i}$(extensionbase)/schemas"; \
glib-compile-schemas "$(builddir)/_build/$${i}$(extensionbase)/schemas"; \
fi; \
(cd "$(builddir)/_build/$${i}$(extensionbase)"; \
zip -qr "$(abs_builddir)/zip-files/$${i}$(extensionbase).shell-extension.zip" .; \
); \
done
-rm -fR $(builddir)/_build
localprefix = $(HOME)/.local/share/gnome-shell/extensions
local-install: zip-file
for i in $(ENABLED_EXTENSIONS); do \
uuid="$${i}$(extensionbase)"; \
zip_file="$(abs_builddir)/zip-files/$${uuid}.shell-extension.zip"; \
if [ -d "$(localprefix)/$${uuid}" ]; then \
rm -fR "$(localprefix)/$${uuid}"; \
fi; \
$(MKDIR_P) $(localprefix)/$${uuid}; \
(cd $(localprefix)/$${uuid}; \
unzip -q $${zip_file}; \
); \
done

578
NEWS
View File

@@ -1,580 +1,6 @@
42.beta
=======
* workspace-indicator: Fix cancelling editing with Esc [Florian; !208]
* window-list: Update window tracking to avoid missing icons [Florian; !207]
* Use libadwaita for preferences [Florian; !209, !213]
* Adapt to Clutter grab API changes [Florian; !212]
* Misc. bug fixes and cleanups [Jan, Florian; !210, !214]
Contributors:
Jan Beich, Florian Müllner, Naala Nanba
Translators:
Boyuan Yang [zh_CN], Matej Urbančič [sl], Naala Nanba [ab],
Alexander Shopov [bg], Emin Tufan Çetin [tr]
42.alpha
========
* native-window-placement: Fix distorted layout in app grid [Sebastian; !189]
* window-list: Fix on-screen keyboard [Florian; !199]
* Misc. bug fixes and cleanups [Neal; Just; !195, !197]
Contributors:
Piotr Drąg, Neal Gompa, Sebastian Keller, Florian Müllner, Just Perfection
Translators:
Goran Vidović [hr], Sveinn í Felli [is], Yuri Chornoivan [uk],
Fabio Tomat [fur], Quentin PAGÈS [oc], Hugo Carvalho [pt],
Yaron Shahrabani [he], Jordi Mas i Hernandez [ca], MohammadSaleh Kamyab [fa],
Fran Dieguez [gl], Daniel Mustieles [es], Aleksandr Melman [ru],
Aurimas Černius [lt], Asier Sarasua Garmendia [eu], Kukuh Syafaat [id],
Rafael Fontenelle [pt_BR]
41.0
====
* Bump version
41.rc.1
=======
* Fix pre-generating stylesheets in tarball [Florian; !190]
Contributors:
Florian Müllner
41.rc
=====
* window-list: Adapt to overview-on-startup [Florian; !185]
* apps-menu: Use a custom 'toggle-menu' shortcut [Florian; !173]
* Misc. bug fixes and cleanups [Florian; !186]
Contributors:
Florian Müllner
41.beta
=======
* window-list: Extend reactive area of minimap to screen edges [Adam; !171]
* drive-menu: Improve detection of network mounts [Florian; !27, !176]
* Use distinct gettext domain for e.g.o uploads [Florian; #335]
* Misc. bug fixes and cleanups [Florian; !172, !174, !177, !167, !178, !180,
!181, !182, !183]
Contributors:
Marco Trevisan (Treviño), Adam Goode, Florian Müllner
Translators:
Hugo Carvalho [pt], Juliano de Souza Camargo [pt], Alexander Shopov [bg]
40.1
====
* Disable welcome dialog in classic session [Florian; !169]
* windowsNavigator: Adjust to a late gnome-shell change [Florian; !170]
Contributors:
Florian Müllner
Translators:
Ngọc Quân Trần [vi], Anders Jonsson [sv], Carmen Bianca BAKKER [eo],
Pawan Chitrakar [ne], Quentin PAGÈS [oc]
40.0
====
Translators:
Jiri Grönroos [fi]
40.rc
=====
* native-window-placement: Adjust to gnome-shell changes [Florian; !164]
* windows-navigator: Adjust to gnome-shell changes [Florian; !163]
* window-list, workspace-indicator: Only show previews for up to six workspaces
[Florian; !165]
* window-list, workspace-indicator: Improve workspace preview appearance
[Florian; !166]
Contributors:
Florian Müllner
Translators:
Fran Dieguez [gl]
40.beta
=======
* Add tooltips to workspace thumbnails [Florian; !155]
* Drop arrows from top bar menus [Florian; !156]
* drive-menu: Mark mounts that can be unmounted as removable [Michael; !152]
* Remove horizontal-workspaces extension [Florian; !158]
* Adjust to shell overview changes [Florian; !159, !160]
* Fix crashes [Daniel; !157]
* Misc. bug fixes and cleanups [Florian; !154, !161]
Contributors:
Michael Lawton, Florian Müllner, Daniel van Vugt
Translators:
Аляксей [be], A S Alam [pa]
40.alpha.1
==========
* Don't depend on sassc when building from tarball [Florian; !150]
* Port extensions preferences to GTK4 [Florian; !148]
* Misc. bug fixes and cleanups [Florian, Jonas; !149, !151, !153]
Contributors:
Jonas Dreßler, Florian Müllner
40.alpha
========
* window-list: Honor changes in skip-taskbar property [Sergio; !130]
* window-list, workspace-indicator: Adjust to 3.38 changes [Florian; !133]
* window-list, workspace-indicator: Improve previews in workspace thumbs
[Florian; #260, !142]
* auto-move: Improve behavior on multi-monitor setups [Florian; !135]
* windowNavigator: Adjust to 3.38 changes [Thun; #259]
* Misc. bug fixes and cleanups [Florian, Jonas Å, Jordan, Ray; !131, !136,
!137, !140, !141, !144, !146, !145]
Contributors:
Sergio Costas, Florian Müllner, Jordan Petridis, Thun Pin, Ray Strode,
Jonas Ådahl
Translators:
Fabio Tomat [fur], Jordi Mas [ca]
3.38.1
3.22.2
======
Contributors:
Yacine Bouklif, Florian Müllner
Translators:
Yacine Bouklif [kab], Cheng-Chia Tseng [zh_TW], Stas Solovey [ru],
Yosef Or Boczko [he]
3.38.0
======
Translators:
Balázs Meskó [hu], Alan Mortensen [da], Juliano Camargo [pt], Tim Sabsch [de],
Milo Casagrande [it], Rūdolfs Mazurs [lv]
3.37.92
=======
Translators:
Nathan Follens [nl], Zander Brown [en_GB], Aurimas Černius [lt],
Marek Černocký [cs], Changwoo Ryu [ko], Dušan Kazik [sk]
3.37.91
=======
Contributors:
Florian Müllner
Translators:
Fran Dieguez [gl], Akarshan Biswas [bn_IN], Kukuh Syafaat [id],
Piotr Drąg [pl], Rafael Fontenelle [pt_BR], Jiri Grönroos [fi],
Марко Костић [sr], Goran Vidović [hr]
3.37.90
=======
* Misc. bug fixes and cleanups [Florian, Piotr; !126, !128]
Contributors:
Piotr Drąg, Florian Müllner
Translators:
Fabio Tomat [fur], Efstathios Iosifidis [el], Anders Jonsson [sv],
Asier Sarasua Garmendia [eu], Alexandre Franke [fr]
3.37.3
======
* window-list, native-window-placement: Adjust to shell changes [Florian; !124]
Contributors:
Florian Müllner
Translators:
Jordi Mas [ca], sicklylife [ja], Boyuan Yang [zh_CN],
Baurzhan Muftakhidinov [kk]
3.37.2
======
* window-list, auto-move: Modernize preference dialogs [Florian; !121]
* Adjust to gnome-shell changes [Florian; !122]
Contributors:
Florian Müllner
Translators:
Cheng-Chia Tseng [zh_TW], Yuri Chornoivan [uk], Daniel Mustieles [es],
Emin Tufan Çetin [tr], Danial Behzadi [fa], Daniel Șerbănescu [ro],
Matej Urbančič [sl]
3.37.1
======
* drive-menu: Emphasize eject buttons [Florian; #223]
* user-theme: Add preference dialog [Florian; !117]
* window-list: Fix inconsistent state in preference dialog [Milan; !119]
* workspace-indicator: Overhaul preference dialog [Florian; !120]
* user-theme: Support session mode styles [Florian; !118]
* Misc. bug fixes and cleanups [Florian, Xiaoguang; !113, !106, !114, !116]
Contributors:
Milan Crha, Florian Müllner, Xiaoguang Wang
Translators:
Daniel Korostil [uk], Yosef Or Boczko [he], Kristjan SCHMIDT [eo],
Dz Chen [zh_CN], Danial Behzadi [fa], Yuri Chornoivan [uk],
Anders Jonsson [sv], Daniel Mustieles [es]
3.36.0
======
Contributors:
Florian Müllner
3.35.91
=======
Contributors:
Florian Müllner
Translators:
Zander Brown [en_GB]
3.35.90
=======
* Adjust to gnome-shell changes [Florian; !100, !101, !102]
* Force single-line window titles in window list [Florian; #202]
* Misc. bug fixes and cleanup [Florian; !104, !105]
Contributors:
Florian Müllner
Translators:
sicklylife [ja], Umarzuki Bin Mochlis Moktar [ms]
3.35.3
======
Translators:
Fran Dieguez [gl]
3.35.2
======
* Adjust to gnome-shell changes [Marco, Florian; !89, !95, !96]
* window-list, workspace-indicator: Exclude DESKTOP windows from previews
[Florian; !93]
* screenshot-window-sizer: Fix cycling through all valid sizes [Willy; !97]
Contributors:
Marco Trevisan (Treviño), Florian Müllner, Willy Stadnick
3.34.1
======
* Adjust to gnome-settings-daemon plugin removals [Xiaoguang; !94]
Contributors:
Florian Müllner, Xiaoguang Wang
Translators:
Nathan Follens [nl], Dušan Kazik [sk], Ask Hjorth Larsen [da],
Yi-Jyun Pan [zh_TW]
3.34.0
======
Translators:
Rafael Fontenelle [pt_BR], Efstathios Iosifidis [el], Milo Casagrande [it],
Sabri Ünal [tr]
3.33.92
=======
Translators:
Марко Костић [sr], Tim Sabsch [de], Rūdolfs Mazurs [lv], Matej Urbančič [sl],
Balázs Úr [hu], Claude Paroz [fr], Fran Dieguez [gl], Changwoo Ryu [ko],
Ryuta Fujii [ja], Fabio Tomat [fur], Goran Vidović [hr]
3.33.91
=======
* Misc. bug fixes and cleanups [Florian; !88, !90, !91, !92]
Contributors:
Florian Müllner
Translators:
Asier Sarasua Garmendia [eu], Anders Jonsson [sv], Marek Černocký [cs],
Kukuh Syafaat [id], Jiri Grönroos [fi], Florentina Mușat [ro],
Aurimas Černius [lt], Daniel Mustieles [es], Piotr Drąg [pl], Jordi Mas [ca],
Danial Behzadi [fa]
3.33.90
=======
* window-list: Support showing windows from all workspaces [Florian; #154]
* Misc. bug fixes and cleanups [Florian; !86, !87]
Contributors:
Florian Müllner
Translators:
Jor Teron [mjw]
3.33.4
======
* Make GNOME Classic more classic:
- Disable GNOME 3 overview [Florian; !69]
- Add window picker button to window list [Florian; !73, !80]
- Style improvements and fixes [Jakub; #169, !82]
- Support horizontal workspace layout in window list [Florian; !70]
- Add draggable previews to window list workspace switcher [Florian; !74]
- Arrange workspaces horizontally [Florian; !72]
* workspace-indicator: Support horizontal workspace layout [Florian; !71]
* workspace-indicator: Add draggable previews [Florian; !77]
* Misc. bug fixes and cleanups [Florian; !75, !76, !79, !78, #168, !84]
Contributors:
Florian Müllner, Jakub Steiner, Jor Teron
Translators:
Jor Teron [mjw]
3.33.3
======
* Misc. bug fixes [Florian, Marco; !67, !68]
Contributors:
Florian Müllner, Marco Trevisan (Treviño)
3.33.2
======
* Misc. bug fixes and cleanups [Florian; !66]
Contributors:
Florian Müllner
3.33.1
======
* Misc. bug fixes [Florian; !64]
Contributors:
Florian Müllner
3.32.1
======
* Fix windowsNavigator extension after ES6 port [Florian; #143]
* screenshot-window-sizer: Add phone screenshot sizes [Adrien; !65]
* Misc. bug fixes and cleanups [Fabian; !62]
Contributors:
Florian Müllner, Adrien Plazas, Fabian P. Schmidt
3.32.0
======
Contributors:
Florian Müllner
Translations:
Victor Ibragimov [tg], Kristjan SCHMIDT [eo], Mart Raudsepp [et]
3.31.92
=======
* Misc. bug fixes and cleanups [Florian; !57, !58, !59, !60]
Contributors:
Florian Müllner
3.31.91
=======
* apps-menu: Remove outdated legacy-tray handling [Florian; !53]
* user-theme: Allow using XDG user data dir [Tomasz; !55]
* Misc. bug fixes and cleanups [Florian; !52, !54, !56]
Contributors:
Tomasz Gąsior, Florian Müllner
Translators:
Matej Urbančič [sl], Gun Chleoc [gd]
3.31.90
=======
* Misc. bug fixes and cleanups [Florian; !49, !50, !51]
Contributors:
Florian Müllner
Translators:
Ryuta Fujii [ja], Charles Monzat [fr], Pieter Schalk Schoeman [af]
3.31.2
======
* Remove obsolete alternate-tab extension [Florian; #786496]
* Adjust to gnome-shell changes [Florian; #113]
Contributors:
Florian Müllner
3.30.1
======
* apps-menu: Fix height on HiDPI systems [Florian; #102]
* window-list: Only switch between windows on active workspace when scrolling
[Florian; #78]
Contributors:
Florian Müllner
3.30.0
======
* Bump version
3.29.91
=======
* Misc. bug fixes [Florian; #90]
Contributors:
Florian Müllner
3.29.90
=======
* Misc. bug fixes [Florian; #786496]
Contributors:
Florian Müllner
3.29.3
======
* Adjust to global.screen removal [Jonas; #759538]
Contributors:
Jonas Ådahl, Florian Müllner
3.29.2
======
* Misc. bug fixes [Florian; #69]
Contributors:
Florian Müllner
3.28.1
======
* Misc. bug fixes [Xiaoguang, Florian; #59, #62]
Contributors:
Florian Müllner, Xiaoguang Wang
Translators:
Dz Chen [zh_CN]
3.28.0
======
Contributors:
Florian Müllner, Xiaoguang Wang
Translators:
Aman Alam [pa], Bruce Cowan [en_GB]
3.27.92
=======
Contributors:
Florian Müllner
Translators:
Piotr Drąg [es], GNOME Translation Robot [gd], Daniel Șerbănescu [ro]
3.27.91
=======
* places-menu: Support unmounting ejectable places [Rémy; #17]
* apps-menu: Support separators and custom sort order [Florian; #27]
* Port to meson [Florian; #31, #45]
* window-list: Fix missing icons on wayland [Florian; #10]
* places-menu: Fix terminating gnome-shell with recent gjs [Florian; #44]
* auto-move: Make it work with wayland windows [Florian; #33]
* Classic theme fixes [Florian, Jonas; #26, #41, #39, #40]
* Require sassc for classic styling [Florian; !28]
* Misc. bug fixes [Piotr, Florian; #772211, #32, #30]
Contributors:
Jeremy Bicha, Piotr Drąg, Jonas Kümmerlin, Rémy Lefevre, Iñigo Martínez,
Florian Müllner
Translators:
Matej Urbančič [sl], Kjartan Maraas [nb]
3.27.1
======
* updated translations (ca@valencia)
3.26.1
======
* native-window-placement: Adjust to gnome-shell changes
* updated translations: el, fa, ru, sv
3.26.0
======
* updated translations (be, bg, ca, da, eu, fi, is, it, ko, lv, ml,
nl, pt_BR, vi, zh_TW)
3.25.91
=======
* updated translations (ca, fr, it, pl, pt_BR, sr, sr@latin, tr)
3.25.90
=======
* updated translations (es, gl, hr, hu, kk, sl, sv, sv)
3.25.4
======
* screenshot-window-sizer: Fix backward cycling
* updated translations (ar, be, ca, cs, de, fur, id, lt, pl, sk)
3.25.3
======
* places-menu: Use mount operation if necessary
* window-list: Respect MWM hints
* updated translations (es, fur, kk)
3.25.2
======
* places-menu: Make URI launching asynchronous
* updated translations (de, fur, hr, hu, id, sl)
3.25.1
======
* apps-menu: Mark copied launchers as trusted
* places-menu: Make icon lookup asynchronous
* updated translations (hr)
3.24.1
======
* apps-menu: Allow creating desktop launchers via DND
* updated translations (el, vi)
3.24.0
======
* updated translations (lv, tr)
3.23.92
=======
* update classic theme
* updated translations (be, ko, ca, da, cs, ru, lt)
3.23.91
=======
* updated translations (de, es, eu, fi, fr, fur, gl, hu, id, it, kk, nb, pl, pt_BR,
sk, sr, sr@latin, sv, uk, zh_TW)
3.23.90
=======
* window-list: Improve styling
* window-list: Hide workspace indicator when there's a single (static) workspace
* new translation (be)
3.23.2
======
* alternateTab: Don't take over 'switch-group' shortcut
* updated translations (zh_CN)
* updated translations (zh_CN)
3.22.1
======

74
README Normal file
View File

@@ -0,0 +1,74 @@
GNOME Shell Extensions is a collection of extensions providing additional
and optional functionality to GNOME Shell.
Since GNOME Shell is not API stable, extensions work only against a very
specific version of the shell, usually the same as this package (see
"configure --version"). The extensions in this package are supported by GNOME
and will be updated to reflect future API changes in GNOME Shell.
For more information about GNOME Shell Extensions
https://wiki.gnome.org/Projects/GnomeShell/Extensions
For general information about GNOME Shell
https://wiki.gnome.org/Projects/GnomeShell
Bugs should be reported at https://bugzilla.gnome.org against the 'gnome-shell'
product, with the 'extensions' component.
Extensions
==========
alternate-tab
Lets you use classic Alt+Tab (window-based instead of app-based) in GNOME Shell.
apps-menu
Lets you reach an application using gnome 2.x style menu on the panel.
auto-move-windows
Lets you manage your workspaces more easily, assigning a specific workspace to
each application as soon as it creates a window, in a manner configurable with a
GSettings key.
dock
Shows a dock-style task switcher on the right side of the screen.
drive-menu
Shows a status menu for rapid unmount and power off of external storage devices
(i.e. pendrives)
example
A minimal example illustrating how to write extensions.
gajim
Integration with Gajim, a Jabber/XMPP instant messaging client.
native-window-placement
An alternative algorithm for layouting the thumbnails in the windows overview, that
more closely reflects the actual positions and sizes.
places-menu
Shows a status Indicator for navigating to Places.
user-theme
Loads a shell theme from ~/.themes/<name>/gnome-shell.
windowsNavigator
Allow keyboard selection of windows and workspaces in overlay mode.
License
=======
GNOME Shell Extensions are distributed under the terms of the GNU General Public License,
version 2 or later. See the COPYING file for details.
Individual extensions may be licensed under different terms, see each source
file for details.

View File

@@ -1,96 +0,0 @@
# GNOME Shell Extensions
GNOME Shell Extensions is a collection of extensions providing additional
and optional functionality to GNOME Shell.
Since GNOME Shell is not API stable, extensions work only against a very
specific version of the shell, usually the same as this package (see
"configure --version"). The extensions in this package are supported by GNOME
and will be updated to reflect future API changes in GNOME Shell.
The GNOME wiki has more information about [GNOME Shell Extensions][project-page],
as well as some general information about [GNOME Shell][shell-page].
Bugs should be reported to the GNOME [bug tracking system][bug-tracker].
## Extensions
* alternate-tab (**OBSOLETE**)
Lets you use classic Alt+Tab (window-based instead of app-based) in GNOME Shell.
This extension is obsolete since GNOME 3.30, see [this blogpost][alternatetab-post]
for further details.
* apps-menu
Lets you reach an application using gnome 2.x style menu on the panel.
* auto-move-windows
Lets you manage your workspaces more easily, assigning a specific workspace to
each application as soon as it creates a window, in a manner configurable with a
GSettings key.
* drive-menu
Shows a status menu for rapid unmount and power off of external storage devices
(i.e. pendrives)
* launch-new-instance
Changes application icons to always launch a new instance when activated.
* native-window-placement
An alternative algorithm for layouting the thumbnails in the windows overview, that
more closely reflects the actual positions and sizes.
* places-menu
Shows a status Indicator for navigating to Places.
* screenshot-window-sizer
Adds a shortcut for resizing the focus window to a size that is suitable for GNOME Software screenshots
* user-theme
Loads a shell theme from ~/.themes/<name>/gnome-shell.
* window-list
Adds a bottom panel with a traditional window list.
* windowsNavigator
Allow keyboard selection of windows and workspaces in overlay mode.
* workspace-indicator
Adds a simple workspace switcher to the top bar.
## Default branch
The default development branch is `main`. If you still have a local
checkout under the old name, use:
```sh
git checkout master
git branch -m master main
git fetch
git branch --unset-upstream
git branch -u origin/master
git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
```
## License
GNOME Shell Extensions are distributed under the terms of the GNU General
Public License, version 2 or later. See the [COPYING file][license] for details.
Individual extensions may be licensed under different terms, see each source
file for details.
[project-page]: https://wiki.gnome.org/Projects/GnomeShell/Extensions
[shell-page]: https://wiki.gnome.org/Projects/GnomeShell
[bug-tracker]: https://gitlab.gnome.org/GNOME/gnome-shell-extensions/issues
[license]: COPYING
[alternatetab-post]: https://blogs.gnome.org/fmuellner/2018/10/11/the-future-of-alternatetab-and-why-you-need-not-worry/

26
autogen.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/sh
# Run this to generate all the initial makefiles, etc.
srcdir=`dirname $0`
test -z "$srcdir" && srcdir=.
test -f $srcdir/configure.ac || {
echo -n "**Error**: Directory "\`$srcdir\'" does not look like the"
echo " top-level gnome-shell-extensions directory"
exit 1
}
# Fetch submodules if needed
if test ! -f data/gnome-shell-sass/COPYING;
then
echo "+ Setting up submodules"
git submodule init
fi
git submodule update
which gnome-autogen.sh || {
echo "You need to install gnome-common from GNOME Git (or from"
echo "your OS vendor's package manager)."
exit 1
}
. gnome-autogen.sh

3
config/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
install-sh
mkinstalldirs
missing

98
configure.ac Normal file
View File

@@ -0,0 +1,98 @@
AC_PREREQ(2.63)
AC_INIT([gnome-shell-extensions],[3.22.2],[https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-shell&component=extensions])
AC_CONFIG_MACRO_DIR([m4])
AC_CONFIG_AUX_DIR([config])
AM_INIT_AUTOMAKE([1.10 dist-xz no-dist-gzip foreign tar-ustar])
m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])])
GETTEXT_PACKAGE=gnome-shell-extensions
AC_SUBST(GETTEXT_PACKAGE)
AC_DEFINE_UNQUOTED(GETTEXT_PACKAGE, "$GETTEXT_PACKAGE",
[The prefix for our gettext translation domains.])
AM_GNU_GETTEXT_VERSION([0.19.6])
AM_GNU_GETTEXT([external])
PKG_PROG_PKG_CONFIG([0.22])
GLIB_GSETTINGS
SHELL_VERSION="$PACKAGE_VERSION"
shell_major=`echo "$PACKAGE_VERSION" | cut -d'.' -f1`
shell_minor=`echo "$PACKAGE_VERSION" | cut -d'.' -f2`
if test "$(($shell_minor % 2))" -eq 0; then
SHELL_VERSION="$shell_major.$shell_minor"
fi
AC_SUBST([SHELL_VERSION])
dnl keep this in alphabetic order
CLASSIC_EXTENSIONS="apps-menu places-menu alternate-tab launch-new-instance window-list"
DEFAULT_EXTENSIONS="$CLASSIC_EXTENSIONS drive-menu screenshot-window-sizer windowsNavigator workspace-indicator"
ALL_EXTENSIONS="$DEFAULT_EXTENSIONS auto-move-windows example native-window-placement user-theme"
AC_SUBST(CLASSIC_EXTENSIONS, [$CLASSIC_EXTENSIONS])
AC_SUBST(ALL_EXTENSIONS, [$ALL_EXTENSIONS])
AC_ARG_ENABLE([extensions],
[AS_HELP_STRING([--enable-extensions],[Space separated list of extensions to enable.
The default is to build and distribute all extensions that can be installed in the home directory and have no external depedencies.
Use "all" to enable all available extensions, or "classic-mode" to enable extensions that restore GNOME 2 functionality.])],
[],
[enable_extensions=$DEFAULT_EXTENSIONS]
)
if test x"$enable_extensions" = xall; then
enable_extensions="$ALL_EXTENSIONS"
enable_classic_mode_default=yes
elif test x"$enable_extensions" = xclassic-mode; then
enable_extensions="$CLASSIC_EXTENSIONS"
enable_classic_mode_default=yes
else
enable_classic_mode_default=no
fi
AC_ARG_ENABLE([classic-mode],
[AS_HELP_STRING([--enable-classic-mode],[Enable installing data files for classic mode.
Defaults to yes if enabling all or classic-mode extensions, and no otherwise.])],
[],
[enable_classic_mode=$enable_classic_mode_default])
AM_CONDITIONAL([CLASSIC_MODE], [test x"$enable_classic_mode" != xno])
ENABLED_EXTENSIONS=
for e in $enable_extensions; do
case $e in
dnl keep this in alphabetic order
alternate-tab|apps-menu|auto-move-windows|drive-menu|example|launch-new-instance|native-window-placement|places-menu|screenshot-window-sizer|user-theme|window-list|windowsNavigator|workspace-indicator)
ENABLED_EXTENSIONS="$ENABLED_EXTENSIONS $e"
;;
*)
AC_MSG_ERROR([invalid extension $e])
esac
done
AC_SUBST(ENABLED_EXTENSIONS, [$ENABLED_EXTENSIONS])
AC_PATH_PROG([SASS],[sass],[])
dnl Please keep this sorted alphabetically
AC_CONFIG_FILES([
data/Makefile
extensions/alternate-tab/Makefile
extensions/apps-menu/Makefile
extensions/auto-move-windows/Makefile
extensions/drive-menu/Makefile
extensions/example/Makefile
extensions/launch-new-instance/Makefile
extensions/native-window-placement/Makefile
extensions/places-menu/Makefile
extensions/screenshot-window-sizer/Makefile
extensions/user-theme/Makefile
extensions/window-list/Makefile
extensions/windowsNavigator/Makefile
extensions/workspace-indicator/Makefile
extensions/Makefile
Makefile
po/Makefile.in
])
AC_OUTPUT

View File

@@ -1,9 +0,0 @@
[org.gnome.mutter:GNOME-Classic]
dynamic-workspaces=false
[org.gnome.desktop.wm.preferences:GNOME-Classic]
button-layout='appmenu:minimize,maximize,close'
[org.gnome.desktop.wm.keybindings:GNOME-Classic]
switch-applications=[]
switch-windows=['<Super>Tab','<Alt>Tab']

View File

@@ -1,2 +1,6 @@
To generate the css files, from the project directory:
sass --sourcemap=none --update .
To update the gnome-shell-sass submodule to latest upstream commit:
git submodule update --rebase

73
data/Makefile.am Normal file
View File

@@ -0,0 +1,73 @@
include $(top_srcdir)/include.mk
sessiondir = $(datadir)/gnome-session/sessions
session_in_files = gnome-classic.session.desktop.in
session_DATA = $(session_in_files:.session.desktop.in=.session)
xsessiondir = $(datadir)/xsessions
xsession_in_files = gnome-classic.desktop.in
xsession_DATA = $(xsession_in_files:.desktop.in=.desktop)
modedir = $(datadir)/gnome-shell/modes
mode_in_files = classic.json.in
mode_DATA = $(mode_in_files:.json.in=.json)
themedir = $(datadir)/gnome-shell/theme
theme_DATA = \
$(srcdir)/calendar-today.svg \
$(srcdir)/classic-process-working.svg \
$(srcdir)/classic-toggle-off-intl.svg \
$(srcdir)/classic-toggle-off-us.svg \
$(srcdir)/classic-toggle-on-intl.svg \
$(srcdir)/classic-toggle-on-us.svg \
$(srcdir)/gnome-classic.css \
$(srcdir)/gnome-classic-high-contrast.css \
$(NULL)
theme_sources = \
$(srcdir)/gnome-classic.scss \
$(srcdir)/gnome-shell-sass/_colors.scss \
$(srcdir)/gnome-shell-sass/_common.scss \
$(srcdir)/gnome-shell-sass/_drawing.scss \
$(srcdir)/gnome-shell-sass/_high-contrast-colors.scss \
$(NULL)
%.css: %.scss $(theme_sources)
@if test -n "$(SASS)"; then \
if $(AM_V_P); then PS4= set -x; else echo " GEN $@"; fi; \
$(SASS) --sourcemap=none --update -f -q $<; \
fi
gsettings_SCHEMAS = org.gnome.shell.extensions.classic-overrides.gschema.xml
@GSETTINGS_RULES@
%.desktop:%.desktop.in
$(AM_V_GEN) $(MSGFMT) --desktop --template $< -d $(top_srcdir)/po -o $@
%.session: %.session.desktop.in
$(AM_V_GEN) $(MSGFMT) --desktop --template $< -d $(top_srcdir)/po -o $@
comma:=,
empty:=
space:= $(empty) $(empty)
extensions:=$(patsubst %,\"%$(extensionbase)\",$(CLASSIC_EXTENSIONS))
extension_list:=$(subst $(space),$(comma),$(extensions))
%.json:%.json.in
$(AM_V_GEN) sed \
-e "s|\@CLASSIC_EXTENSIONS\@|$(extension_list)|g" \
$< > $@
EXTRA_DIST = \
$(session_in_files) \
$(xsession_in_files) \
$(mode_in_files) \
$(gsettings_SCHEMAS) \
$(theme_DATA) \
$(NULL)
CLEANFILES = \
$(session_DATA) \
$(xsession_DATA) \
$(mode_DATA) \
$(NULL)

View File

@@ -1,10 +1,8 @@
{
"parentMode": "user",
"stylesheetName": "gnome-classic.css",
"hasOverview": false,
"showWelcomeDialog": false,
"enabledExtensions": [@CLASSIC_EXTENSIONS@],
"panel": { "left": ["appMenu"],
"panel": { "left": ["activities", "appMenu"],
"center": [],
"right": ["a11y", "keyboard", "dateMenu", "aggregateMenu"]
}

View File

@@ -1,8 +0,0 @@
[Desktop Entry]
Name=GNOME Classic on Wayland
Comment=This session logs you into GNOME Classic
Exec=env GNOME_SHELL_SESSION_MODE=classic gnome-session
TryExec=gnome-session
Type=Application
DesktopNames=GNOME-Classic;GNOME;
X-GDM-SessionRegisters=true

View File

@@ -1,8 +0,0 @@
[Desktop Entry]
Name=GNOME Classic on Xorg
Comment=This session logs you into GNOME Classic
Exec=env GNOME_SHELL_SESSION_MODE=classic gnome-session
TryExec=gnome-session
Type=Application
DesktopNames=GNOME-Classic;GNOME;
X-GDM-SessionRegisters=true

1926
data/gnome-classic.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
[Desktop Entry]
Name=GNOME Classic
Comment=This session logs you into GNOME Classic
Exec=env GNOME_SHELL_SESSION_MODE=classic gnome-session
Exec=env GNOME_SHELL_SESSION_MODE=classic gnome-session --session gnome-classic
TryExec=gnome-session
Type=Application
DesktopNames=GNOME-Classic;GNOME;
X-GDM-SessionRegisters=true

View File

@@ -4,11 +4,10 @@ $variant: 'light';
@import "gnome-shell-sass/_colors"; //use gtk colors
@import "gnome-shell-sass/_drawing";
@import "gnome-shell-sass/_common";
@import "gnome-shell-sass/_widgets";
/* Overrides */
#panel, #panel.solid {
#panel {
font-weight: normal;
background-color: $bg_color;
background-gradient-direction: vertical;
@@ -32,38 +31,15 @@ $variant: 'light';
-minimum-hpadding: 4px;
font-weight: normal;
color: $fg_color;
text-shadow: none;
transition-duration: 0ms;
border: 0;
border-radius: 0px;
&.clock-display {
.clock {
transition-duration: 0ms;
border: 0;
border-radius: 0px;
}
}
&:hover {
color: lighten($fg_color,10%);
text-shadow: none;
& .system-status-icon { icon-shadow: none; }
}
&:active, &:overview, &:focus, &:checked {
// Trick due to St limitations. It needs a background to draw
// a box-shadow
background-color: $selected_bg_color;
color: $selected_fg_color;
background-color: $selected_bg_color !important;
color: $selected_fg_color !important;
box-shadow: none;
& > .system-status-icon { icon-shadow: none; }
}
.app-menu-icon { width: 0; height: 0; margin: 0; } // shell's display:none; :D
.system-status-icon {
icon-shadow: none;
}
}
.panel-corner,
@@ -75,17 +51,18 @@ $variant: 'light';
&.lock-screen,
&.unlock-screen,
&.login-screen {
background-color: transparentize($bg_color, 0.5);
background-gradient-start: transparentize($bg_color, 0.5);
background-gradient-end: transparentize($bg_color, 0.5);
background-color: transparentize($_bubble_bg_color, 0.5);
background-gradient-start: transparentize($_bubble_bg_color, 0.5);
background-gradient-end: transparentize($_bubble_bg_color, 0.5);
border-bottom: none;
.panel-button { color: $osd_fg_color; }
}
.popup-menu-arrow { width: 0; height: 0; } // shell's display: none;
}
#appMenu {
padding: 0 8px 0 8px;
spinner-image: url("classic-process-working.svg");
.panel-status-menu-box { padding: 0; }
}
.tile-preview-left.on-primary,
.tile-preview-right.on-primary,
@@ -104,4 +81,3 @@ $variant: 'light';
.calendar-day-with-events {
background-image: url("calendar-today.svg");
}

View File

@@ -0,0 +1,3 @@
[GNOME Session]
Name=GNOME Classic
RequiredComponents=org.gnome.Shell;gnome-settings-daemon;nautilus-classic;

View File

@@ -1,119 +0,0 @@
session_desktop_base = 'gnome-classic'
session_desktops = [
session_desktop_base,
session_desktop_base + '-xorg',
session_desktop_base + '-wayland',
]
foreach name: session_desktops
session_desktop = name + '.desktop'
if name.endswith('-xorg')
session_instdir = xsessiondir
elif name.endswith('-wayland')
session_instdir = wlsessiondir
else
# FIXME: The same target can not be copied into two directories.
# There is a workaround in meson/session-post-install.py until proper
# solution arises:
# https://github.com/mesonbuild/meson/issues/2416
session_instdir = xsessiondir
#session_instdir = [ xesssiondir, wlsessiondir ]
endif
i18n.merge_file(
input: session_desktop + '.in',
output: session_desktop,
po_dir: '../po',
install: true,
install_dir: session_instdir,
type: 'desktop'
)
endforeach
classic_uuids = []
foreach e : classic_extensions
classic_uuids += e + uuid_suffix
endforeach
mode_conf = configuration_data()
mode_conf.set('CLASSIC_EXTENSIONS', '"' + '", "'.join(classic_uuids) + '"')
mode_file = 'classic.json'
configure_file(
input: mode_file + '.in',
output: mode_file,
configuration: mode_conf,
install_dir: modedir
)
theme_sources = files(
'gnome-shell-sass/_colors.scss',
'gnome-shell-sass/_common.scss',
'gnome-shell-sass/_drawing.scss',
'gnome-shell-sass/_high-contrast-colors.scss',
'gnome-shell-sass/_widgets.scss',
'gnome-shell-sass/widgets/_a11y.scss',
'gnome-shell-sass/widgets/_app-grid.scss',
'gnome-shell-sass/widgets/_base.scss',
'gnome-shell-sass/widgets/_buttons.scss',
'gnome-shell-sass/widgets/_calendar.scss',
'gnome-shell-sass/widgets/_check-box.scss',
'gnome-shell-sass/widgets/_corner-ripple.scss',
'gnome-shell-sass/widgets/_dash.scss',
'gnome-shell-sass/widgets/_dialogs.scss',
'gnome-shell-sass/widgets/_entries.scss',
'gnome-shell-sass/widgets/_hotplug.scss',
'gnome-shell-sass/widgets/_ibus-popup.scss',
'gnome-shell-sass/widgets/_keyboard.scss',
'gnome-shell-sass/widgets/_login-dialog.scss',
'gnome-shell-sass/widgets/_looking-glass.scss',
'gnome-shell-sass/widgets/_message-list.scss',
'gnome-shell-sass/widgets/_misc.scss',
'gnome-shell-sass/widgets/_network-dialog.scss',
'gnome-shell-sass/widgets/_notifications.scss',
'gnome-shell-sass/widgets/_osd.scss',
'gnome-shell-sass/widgets/_overview.scss',
'gnome-shell-sass/widgets/_panel.scss',
'gnome-shell-sass/widgets/_popovers.scss',
'gnome-shell-sass/widgets/_screen-shield.scss',
'gnome-shell-sass/widgets/_scrollbars.scss',
'gnome-shell-sass/widgets/_search-entry.scss',
'gnome-shell-sass/widgets/_search-results.scss',
'gnome-shell-sass/widgets/_slider.scss',
'gnome-shell-sass/widgets/_switcher-popup.scss',
'gnome-shell-sass/widgets/_switches.scss',
'gnome-shell-sass/widgets/_tiled-previews.scss',
'gnome-shell-sass/widgets/_window-picker.scss',
'gnome-shell-sass/widgets/_workspace-switcher.scss',
'gnome-shell-sass/widgets/_workspace-thumbnails.scss'
)
theme_data = [
'calendar-today.svg',
'classic-process-working.svg',
'classic-toggle-off-intl.svg',
'classic-toggle-off-us.svg',
'classic-toggle-on-intl.svg',
'classic-toggle-on-us.svg',
'gnome-classic-high-contrast.css'
]
stylesheet = 'gnome-classic.css'
if fs.exists(stylesheet)
install_data(stylesheet, install_dir: themedir)
else
sassc = find_program('sassc', required: true)
custom_target(stylesheet,
input: fs.replace_suffix(stylesheet, '.scss'),
output: stylesheet,
depend_files: theme_sources,
command: [sassc, '-a', '@INPUT@', '@OUTPUT@'],
install: true,
install_dir: themedir
)
endif
install_data(theme_data, install_dir: themedir)
classic_override = '00_org.gnome.shell.extensions.classic.gschema.override'
install_data(classic_override, install_dir: schemadir)

View File

@@ -0,0 +1,46 @@
<schemalist>
<schema id="org.gnome.shell.extensions.classic-overrides"
path="/org/gnome/shell/extensions/classic-overrides/"
gettext-domain="gnome-shell-extensions">
<key name="attach-modal-dialogs" type="b">
<default>true</default>
<summary>Attach modal dialog to the parent window</summary>
<description>
This key overrides the key in org.gnome.mutter when running
GNOME Shell.
</description>
</key>
<key name="button-layout" type="s">
<default>"appmenu:minimize,maximize,close"</default>
<summary>Arrangement of buttons on the titlebar</summary>
<description>
This key overrides the key in org.gnome.desktop.wm.preferences when running GNOME Shell.
</description>
</key>
<key name="edge-tiling" type="b">
<default>true</default>
<summary>Enable edge tiling when dropping windows on screen edges</summary>
<description>
This key overrides the key in org.gnome.mutter when running GNOME Shell.
</description>
</key>
<key name="workspaces-only-on-primary" type="b">
<default>true</default>
<summary>Workspaces only on primary monitor</summary>
<description>
This key overrides the key in org.gnome.mutter when running GNOME Shell.
</description>
</key>
<key name="focus-change-on-pointer-rest" type="b">
<default>true</default>
<summary>Delay focus changes in mouse mode until the pointer stops moving</summary>
<description>
This key overrides the key in org.gnome.mutter when running GNOME Shell.
</description>
</key>
</schema>
</schemalist>

View File

@@ -1,47 +0,0 @@
#!/bin/sh
srcdir=`dirname $0`
srcdir=`(cd $srcdir && pwd)`
builddir=`mktemp -p $srcdir -d _build.XXXXXX` || exit 1
installdir=`mktemp -p $srcdir -d _install.XXXXXX` || exit 1
meson setup --prefix=$installdir -Dextension_set=all $srcdir $builddir
meson install -C $builddir
rm -rf $srcdir/zip-files
mkdir $srcdir/zip-files
extensiondir=$installdir/share/gnome-shell/extensions
schemadir=$installdir/share/glib-2.0/schemas
for f in $extensiondir/*; do
name=`basename ${f%%@*}`
uuid=$name@gnome-shell-extensions.gcampax.github.com
schema=$schemadir/org.gnome.shell.extensions.$name.gschema.xml
olddomain=gnome-shell-extensions
newdomain=gnome-shell-extension-$name
sed -i "/gettext-domain/ s:$olddomain:$newdomain:" $f/metadata.json
xgettext --from-code=UTF-8 --output-dir=$builddir --output=$name.pot $f/*.js
if [ -f $builddir/$name.pot ]; then
mkdir $f/po
for l in $(<$srcdir/po/LINGUAS); do
msgmerge --quiet --output-file=$f/po/$l.po \
$srcdir/po/$l.po $builddir/$name.pot
done
fi
cp $srcdir/NEWS $srcdir/COPYING $f
sources=(NEWS COPYING $(cd $f; ls *.js))
[ -f $schema ] || unset schema
gnome-extensions pack ${sources[@]/#/--extra-source=} \
${schema:+--schema=$schema} --out-dir=$srcdir/zip-files $f
done
rm -rf $builddir
rm -rf $installdir

18
extension.mk Normal file
View File

@@ -0,0 +1,18 @@
include $(top_srcdir)/include.mk
dist_extension_DATA = extension.js stylesheet.css $(EXTRA_MODULES)
nodist_extension_DATA = metadata.json $(top_srcdir)/lib/convenience.js $(EXTRA_EXTENSION)
EXTRA_DIST = metadata.json.in
metadata.json: metadata.json.in $(top_builddir)/config.status
$(AM_V_GEN) sed \
-e "s|[@]extension_id@|$(EXTENSION_ID)|" \
-e "s|[@]uuid@|$(uuid)|" \
-e "s|[@]gschemaname@|$(gschemaname)|" \
-e "s|[@]gettext_domain@|$(GETTEXT_PACKAGE)|" \
-e "s|[@]shell_current@|$(SHELL_VERSION)|" \
-e "s|[@]url@|$(extensionurl)|" \
$< > $@
CLEANFILES = metadata.json

View File

3
extensions/Makefile.am Normal file
View File

@@ -0,0 +1,3 @@
DIST_SUBDIRS = $(ALL_EXTENSIONS)
SUBDIRS = $(ENABLED_EXTENSIONS)

View File

@@ -0,0 +1,5 @@
EXTENSION_ID = alternate-tab
EXTRA_MODULES = prefs.js
include ../../extension.mk

View File

@@ -0,0 +1,65 @@
/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const AltTab = imports.ui.altTab;
const Main = imports.ui.main;
const WindowManager = imports.ui.windowManager;
let injections = {};
function init(metadata) {
}
function setKeybinding(name, func) {
Main.wm.setCustomKeybindingHandler(name, Shell.ActionMode.NORMAL, func);
}
function enable() {
injections['_keyPressHandler'] = AltTab.WindowSwitcherPopup.prototype._keyPressHandler;
AltTab.WindowSwitcherPopup.prototype._keyPressHandler = function(keysym, action) {
switch(action) {
case Meta.KeyBindingAction.SWITCH_APPLICATIONS:
case Meta.KeyBindingAction.SWITCH_GROUP:
action = Meta.KeyBindingAction.SWITCH_WINDOWS;
break;
case Meta.KeyBindingAction.SWITCH_APPLICATIONS_BACKWARD:
case Meta.KeyBindingAction.SWITCH_GROUP_BACKWARD:
action = Meta.KeyBindingAction.SWITCH_WINDOWS_BACKWARD;
break;
}
return injections['_keyPressHandler'].call(this, keysym, action);
};
Main.wm._forcedWindowSwitcher = function(display, screen, window, binding) {
/* prevent a corner case where both popups show up at once */
if (this._workspaceSwitcherPopup != null)
this._workspaceSwitcherPopup.destroy();
let tabPopup = new AltTab.WindowSwitcherPopup();
if (!tabPopup.show(binding.is_reversed(), binding.get_name(), binding.get_mask()))
tabPopup.destroy();
};
setKeybinding('switch-applications', Lang.bind(Main.wm, Main.wm._forcedWindowSwitcher));
setKeybinding('switch-group', Lang.bind(Main.wm, Main.wm._forcedWindowSwitcher));
setKeybinding('switch-applications-backward', Lang.bind(Main.wm, Main.wm._forcedWindowSwitcher));
setKeybinding('switch-group-backward', Lang.bind(Main.wm, Main.wm._forcedWindowSwitcher));
}
function disable() {
var prop;
setKeybinding('switch-applications', Lang.bind(Main.wm, Main.wm._startSwitcher));
setKeybinding('switch-group', Lang.bind(Main.wm, Main.wm._startSwitcher));
setKeybinding('switch-applications-backward', Lang.bind(Main.wm, Main.wm._startSwitcher));
setKeybinding('switch-group-backward', Lang.bind(Main.wm, Main.wm._startSwitcher));
for (prop in injections)
AltTab.WindowSwitcherPopup.prototype[prop] = injections[prop];
delete Main.wm._forcedWindowSwitcher;
}

View File

@@ -0,0 +1,11 @@
{
"extension-id": "@extension_id@",
"uuid": "@uuid@",
"settings-schema": "@gschemaname@",
"gettext-domain": "@gettext_domain@",
"name": "AlternateTab",
"description": "Substitute Alt-Tab with a window based switcher that does not group by application.\nThis extension is part of Classic Mode and is officially supported by GNOME. Please do not report bugs using the form below, use GNOME Bugzilla instead.",
"original-authors": [ "jw@bargsten.org", "thomas.bouffon@gmail.com" ],
"shell-version": [ "@shell_current@" ],
"url": "@url@"
}

View File

@@ -0,0 +1,85 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const GObject = imports.gi.GObject;
const Lang = imports.lang;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const N_ = function(e) { return e };
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
const SETTINGS_APP_ICON_MODE = 'app-icon-mode';
const SETTINGS_CURRENT_WORKSPACE_ONLY = 'current-workspace-only';
const MODES = {
'thumbnail-only': N_("Thumbnail only"),
'app-icon-only': N_("Application icon only"),
'both': N_("Thumbnail and application icon"),
};
const AltTabSettingsWidget = new GObject.Class({
Name: 'AlternateTab.Prefs.AltTabSettingsWidget',
GTypeName: 'AltTabSettingsWidget',
Extends: Gtk.Grid,
_init : function(params) {
this.parent(params);
this.margin = 24;
this.row_spacing = 6;
this.orientation = Gtk.Orientation.VERTICAL;
this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell.window-switcher' });
let presentLabel = '<b>' + _("Present windows as") + '</b>';
this.add(new Gtk.Label({ label: presentLabel, use_markup: true,
halign: Gtk.Align.START }));
let align = new Gtk.Alignment({ left_padding: 12 });
this.add(align);
let grid = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL,
row_spacing: 6,
column_spacing: 6 });
align.add(grid);
let radio = null;
let currentMode = this._settings.get_string(SETTINGS_APP_ICON_MODE);
for (let mode in MODES) {
// copy the mode variable because it has function scope, not block scope
// so cannot be used in a closure
let modeCapture = mode;
let name = Gettext.gettext(MODES[mode]);
radio = new Gtk.RadioButton({ group: radio, label: name, valign: Gtk.Align.START });
radio.connect('toggled', Lang.bind(this, function(widget) {
if (widget.active)
this._settings.set_string(SETTINGS_APP_ICON_MODE, modeCapture);
}));
grid.add(radio);
if (mode == currentMode)
radio.active = true;
}
let check = new Gtk.CheckButton({ label: _("Show only windows in the current workspace"),
margin_top: 6 });
this._settings.bind(SETTINGS_CURRENT_WORKSPACE_ONLY, check, 'active', Gio.SettingsBindFlags.DEFAULT);
this.add(check);
},
});
function init() {
Convenience.initTranslations();
}
function buildPrefsWidget() {
let widget = new AltTabSettingsWidget();
widget.show_all();
return widget;
}

View File

@@ -0,0 +1 @@
/* This extensions requires no special styling */

View File

@@ -0,0 +1,3 @@
EXTENSION_ID = apps-menu
include ../../extension.mk

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)
extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')

View File

@@ -1,10 +1,9 @@
{
"extension-id": "@extension_id@",
"uuid": "@uuid@",
"settings-schema": "@gschemaname@",
"gettext-domain": "@gettext_domain@",
"name": "Applications Menu",
"description": "Add a category-based menu for applications.\nThis extension is part of Classic Mode and is officially supported by GNOME. Please do not report bugs using the form below, use GNOME's GitLab instance instead.",
"description": "Add a category-based menu for applications.\nThis extension is part of Classic Mode and is officially supported by GNOME. Please do not report bugs using the form below, use GNOME Bugzilla instead.",
"original-authors": [ "e2002@bk.ru", "debarshir@gnome.org" ],
"shell-version": [ "@shell_current@" ],
"url": "@url@"

View File

@@ -1,12 +0,0 @@
<schemalist gettext-domain="gnome-shell-extensions">
<schema id="org.gnome.shell.extensions.apps-menu"
path="/org/gnome/shell/extensions/apps-menu/">
<key name="apps-menu-toggle-menu" type="as">
<default>["&lt;Alt&gt;F1"]</default>
<summary>Keybinding to open the applications menu</summary>
<description>
Keybinding to open the applications menu.
</description>
</key>
</schema>
</schemalist>

View File

@@ -0,0 +1,7 @@
EXTENSION_ID = auto-move-windows
EXTRA_MODULES = prefs.js
include ../../extension.mk
include ../../settings.mk

View File

@@ -1,142 +1,152 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
// Start apps on custom workspaces
/* exported init enable disable */
const { Shell } = imports.gi;
const Glib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
class WindowMover {
constructor() {
this._settings = ExtensionUtils.getSettings();
this._appSystem = Shell.AppSystem.get_default();
this._appConfigs = new Map();
this._appData = new Map();
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
this._appsChangedId =
this._appSystem.connect('installed-changed',
this._updateAppData.bind(this));
const SETTINGS_KEY = 'application-list';
this._settings.connect('changed', this._updateAppConfigs.bind(this));
this._updateAppConfigs();
}
let settings;
_updateAppConfigs() {
this._appConfigs.clear();
const WindowMover = new Lang.Class({
Name: 'AutoMoveWindows.WindowMover',
this._settings.get_strv('application-list').forEach(v => {
let [appId, num] = v.split(':');
this._appConfigs.set(appId, parseInt(num) - 1);
});
_init: function() {
this._settings = settings;
this._windowTracker = Shell.WindowTracker.get_default();
this._updateAppData();
}
let display = global.screen.get_display();
// Connect after so the handler from ShellWindowTracker has already run
this._windowCreatedId = display.connect_after('window-created', Lang.bind(this, this._findAndMove));
},
_updateAppData() {
let ids = [...this._appConfigs.keys()];
let removedApps = [...this._appData.keys()]
.filter(a => !ids.includes(a.id));
removedApps.forEach(app => {
app.disconnect(this._appData.get(app).windowsChangedId);
this._appData.delete(app);
});
let addedApps = ids
.map(id => this._appSystem.lookup_app(id))
.filter(app => app && !this._appData.has(app));
addedApps.forEach(app => {
let data = {
windowsChangedId: app.connect('windows-changed',
this._appWindowsChanged.bind(this)),
moveWindowsId: 0,
windows: app.get_windows(),
};
this._appData.set(app, data);
});
}
destroy() {
if (this._appsChangedId) {
this._appSystem.disconnect(this._appsChangedId);
this._appsChangedId = 0;
destroy: function() {
if (this._windowCreatedId) {
global.screen.get_display().disconnect(this._windowCreatedId);
this._windowCreatedId = 0;
}
},
if (this._settings) {
this._settings.run_dispose();
this._settings = null;
_ensureAtLeastWorkspaces: function(num, window) {
for (let j = global.screen.n_workspaces; j <= num; j++) {
window.change_workspace_by_index(j-1, false);
global.screen.append_new_workspace(false, 0);
}
},
this._appConfigs.clear();
this._updateAppData();
}
_moveWindow(window, workspaceNum) {
if (window.skip_taskbar || window.is_on_all_workspaces())
_findAndMove: function(display, window, noRecurse) {
if (window.skip_taskbar)
return;
// ensure we have the required number of workspaces
let workspaceManager = global.workspace_manager;
for (let i = workspaceManager.n_workspaces; i <= workspaceNum; i++) {
window.change_workspace_by_index(i - 1, false);
workspaceManager.append_new_workspace(false, 0);
let spaces = this._settings.get_strv(SETTINGS_KEY);
let app = this._windowTracker.get_window_app(window);
if (!app) {
if (!noRecurse) {
// window is not tracked yet
Mainloop.idle_add(Lang.bind(this, function() {
this._findAndMove(display, window, true);
return false;
}));
} else
log ('Cannot find application for window');
return;
}
let app_id = app.get_id();
for ( let j = 0 ; j < spaces.length; j++ ) {
let apps_to_space = spaces[j].split(":");
// Match application id
if (apps_to_space[0] == app_id) {
let workspace_num = parseInt(apps_to_space[1]) - 1;
window.change_workspace_by_index(workspaceNum, false);
if (workspace_num >= global.screen.n_workspaces)
this._ensureAtLeastWorkspaces(workspace_num, window);
window.change_workspace_by_index(workspace_num, false);
}
}
}
_appWindowsChanged(app) {
let data = this._appData.get(app);
let windows = app.get_windows();
// If get_compositor_private() returns non-NULL on a removed windows,
// the window still exists and is just moved to a different workspace
// or something; assume it'll be added back immediately, so keep it
// to avoid moving it again
windows.push(...data.windows.filter(w => {
return !windows.includes(w) && w.get_compositor_private() !== null;
}));
let workspaceNum = this._appConfigs.get(app.id);
windows.filter(w => !data.windows.includes(w)).forEach(window => {
this._moveWindow(window, workspaceNum);
});
data.windows = windows;
}
}
});
let prevCheckWorkspaces;
let winMover;
/** */
function init() {
ExtensionUtils.initTranslations();
Convenience.initTranslations();
settings = Convenience.getSettings();
}
/**
* @returns {bool} - false (used as MetaLater handler)
*/
function myCheckWorkspaces() {
let keepAliveWorkspaces = [];
let foundNonEmpty = false;
for (let i = this._workspaces.length - 1; i >= 0; i--) {
if (!foundNonEmpty) {
foundNonEmpty = this._workspaces[i].list_windows().some(
w => !w.is_on_all_workspaces());
} else if (!this._workspaces[i]._keepAliveId) {
keepAliveWorkspaces.push(this._workspaces[i]);
}
let i;
let emptyWorkspaces = new Array(this._workspaces.length);
if (!Meta.prefs_get_dynamic_workspaces()) {
this._checkWorkspacesId = 0;
return false;
}
// make sure the original method only removes empty workspaces at the end
keepAliveWorkspaces.forEach(ws => (ws._keepAliveId = 1));
prevCheckWorkspaces.call(this);
keepAliveWorkspaces.forEach(ws => delete ws._keepAliveId);
for (i = 0; i < this._workspaces.length; i++) {
let lastRemoved = this._workspaces[i]._lastRemovedWindow;
if ((lastRemoved &&
(lastRemoved.get_window_type() == Meta.WindowType.SPLASHSCREEN ||
lastRemoved.get_window_type() == Meta.WindowType.DIALOG ||
lastRemoved.get_window_type() == Meta.WindowType.MODAL_DIALOG)) ||
this._workspaces[i]._keepAliveId)
emptyWorkspaces[i] = false;
else
emptyWorkspaces[i] = true;
}
let sequences = Shell.WindowTracker.get_default().get_startup_sequences();
for (i = 0; i < sequences.length; i++) {
let index = sequences[i].get_workspace();
if (index >= 0 && index <= global.screen.n_workspaces)
emptyWorkspaces[index] = false;
}
let windows = global.get_window_actors();
for (i = 0; i < windows.length; i++) {
let winActor = windows[i];
let win = winActor.meta_window;
if (win.is_on_all_workspaces())
continue;
let workspaceIndex = win.get_workspace().index();
emptyWorkspaces[workspaceIndex] = false;
}
// If we don't have an empty workspace at the end, add one
if (!emptyWorkspaces[emptyWorkspaces.length -1]) {
global.screen.append_new_workspace(false, global.get_current_time());
emptyWorkspaces.push(false);
}
let activeWorkspaceIndex = global.screen.get_active_workspace_index();
emptyWorkspaces[activeWorkspaceIndex] = false;
// Delete other empty workspaces; do it from the end to avoid index changes
for (i = emptyWorkspaces.length - 2; i >= 0; i--) {
if (emptyWorkspaces[i])
global.screen.remove_workspace(this._workspaces[i], global.get_current_time());
else
break;
}
this._checkWorkspacesId = 0;
return false;
}
/** */
function enable() {
prevCheckWorkspaces = Main.wm._workspaceTracker._checkWorkspaces;
Main.wm._workspaceTracker._checkWorkspaces = myCheckWorkspaces;
@@ -144,7 +154,6 @@ function enable() {
winMover = new WindowMover();
}
/** */
function disable() {
Main.wm._workspaceTracker._checkWorkspaces = prevCheckWorkspaces;
winMover.destroy();

View File

@@ -1,8 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)
extension_sources += files('prefs.js')
extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')

View File

@@ -1,287 +1,274 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
// Start apps on custom workspaces
/* exported init buildPrefsWidget */
const { Adw, Gio, GLib, GObject, Gtk } = imports.gi;
const GdkPixbuf = imports.gi.GdkPixbuf;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const GMenu = imports.gi.GMenu;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const N_ = function(e) { return e };
const ExtensionUtils = imports.misc.extensionUtils;
const _ = ExtensionUtils.gettext;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
const SETTINGS_KEY = 'application-list';
const WORKSPACE_MAX = 36; // compiled in limit of mutter
const AutoMoveSettingsWidget = GObject.registerClass(
class AutoMoveSettingsWidget extends Adw.PreferencesGroup {
_init() {
super._init({
title: _('Workspace Rules'),
});
const Columns = {
APPINFO: 0,
DISPLAY_NAME: 1,
ICON: 2,
WORKSPACE: 3,
ADJUSTMENT: 4
};
this._list = new Gtk.ListBox({
selection_mode: Gtk.SelectionMode.NONE,
css_classes: ['boxed-list'],
});
this.add(this._list);
const Widget = new GObject.Class({
Name: 'AutoMoveWindows.Prefs.Widget',
GTypeName: 'AutoMoveWindowsPrefsWidget',
Extends: Gtk.Grid,
this._list.append(new NewRuleRow());
_init: function(params) {
this.parent(params);
this.set_orientation(Gtk.Orientation.VERTICAL);
this._actionGroup = new Gio.SimpleActionGroup();
this._list.insert_action_group('rules', this._actionGroup);
this._settings = Convenience.getSettings();
this._settings.connect('changed', Lang.bind(this, this._refresh));
this._changedPermitted = false;
let action;
action = new Gio.SimpleAction({ name: 'add' });
action.connect('activate', this._onAddActivated.bind(this));
this._actionGroup.add_action(action);
this._store = new Gtk.ListStore();
this._store.set_column_types([Gio.AppInfo, GObject.TYPE_STRING, Gio.Icon, GObject.TYPE_INT,
Gtk.Adjustment]);
action = new Gio.SimpleAction({
name: 'remove',
parameter_type: new GLib.VariantType('s'),
});
action.connect('activate', this._onRemoveActivated.bind(this));
this._actionGroup.add_action(action);
let scrolled = new Gtk.ScrolledWindow({ shadow_type: Gtk.ShadowType.IN});
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
this.add(scrolled);
action = new Gio.SimpleAction({ name: 'update' });
action.connect('activate', () => {
this._settings.set_strv(SETTINGS_KEY,
this._getRuleRows().map(row => `${row.id}:${row.value}`));
});
this._actionGroup.add_action(action);
this._updateAction = action;
this._settings = ExtensionUtils.getSettings();
this._changedId = this._settings.connect('changed',
this._sync.bind(this));
this._sync();
this._treeView = new Gtk.TreeView({ model: this._store,
hexpand: true, vexpand: true });
this._treeView.get_selection().set_mode(Gtk.SelectionMode.SINGLE);
this.connect('destroy', () => this._settings.run_dispose());
}
let appColumn = new Gtk.TreeViewColumn({ expand: true, sort_column_id: Columns.DISPLAY_NAME,
title: _("Application") });
let iconRenderer = new Gtk.CellRendererPixbuf;
appColumn.pack_start(iconRenderer, false);
appColumn.add_attribute(iconRenderer, "gicon", Columns.ICON);
let nameRenderer = new Gtk.CellRendererText;
appColumn.pack_start(nameRenderer, true);
appColumn.add_attribute(nameRenderer, "text", Columns.DISPLAY_NAME);
this._treeView.append_column(appColumn);
_onAddActivated() {
const dialog = new NewRuleDialog(this.get_root());
dialog.connect('response', (dlg, id) => {
const appInfo = id === Gtk.ResponseType.OK
? dialog.get_widget().get_app_info() : null;
if (appInfo) {
this._settings.set_strv(SETTINGS_KEY, [
...this._settings.get_strv(SETTINGS_KEY),
`${appInfo.get_id()}:1`,
]);
}
dialog.destroy();
});
dialog.show();
}
let workspaceColumn = new Gtk.TreeViewColumn({ title: _("Workspace"),
sort_column_id: Columns.WORKSPACE });
let workspaceRenderer = new Gtk.CellRendererSpin({ editable: true });
workspaceRenderer.connect('edited', Lang.bind(this, this._workspaceEdited));
workspaceColumn.pack_start(workspaceRenderer, true);
workspaceColumn.add_attribute(workspaceRenderer, "adjustment", Columns.ADJUSTMENT);
workspaceColumn.add_attribute(workspaceRenderer, "text", Columns.WORKSPACE);
this._treeView.append_column(workspaceColumn);
_onRemoveActivated(action, param) {
const removed = param.deepUnpack();
this._settings.set_strv(SETTINGS_KEY,
this._settings.get_strv(SETTINGS_KEY).filter(entry => {
const [id] = entry.split(':');
return id !== removed;
scrolled.add(this._treeView);
let toolbar = new Gtk.Toolbar({ icon_size: Gtk.IconSize.SMALL_TOOLBAR });
toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);
this.add(toolbar);
let newButton = new Gtk.ToolButton({ icon_name: 'bookmark-new-symbolic',
label: _("Add Rule"),
is_important: true });
newButton.connect('clicked', Lang.bind(this, this._createNew));
toolbar.add(newButton);
let delButton = new Gtk.ToolButton({ icon_name: 'edit-delete-symbolic' });
delButton.connect('clicked', Lang.bind(this, this._deleteSelected));
toolbar.add(delButton);
let selection = this._treeView.get_selection();
selection.connect('changed',
function() {
delButton.sensitive = selection.count_selected_rows() > 0;
});
delButton.sensitive = selection.count_selected_rows() > 0;
this._changedPermitted = true;
this._refresh();
},
_createNew: function() {
let dialog = new Gtk.Dialog({ title: _("Create new matching rule"),
transient_for: this.get_toplevel(),
use_header_bar: true,
modal: true });
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL);
let addButton = dialog.add_button(_("Add"), Gtk.ResponseType.OK);
dialog.set_default_response(Gtk.ResponseType.OK);
let grid = new Gtk.Grid({ column_spacing: 10,
row_spacing: 15,
margin: 10 });
dialog._appChooser = new Gtk.AppChooserWidget({ show_all: true });
dialog._appChooser.connect('application-selected', Lang.bind(this,
function(w, appInfo) {
addButton.sensitive = appInfo &&
this._checkId(appInfo.get_id());
}));
}
let appInfo = dialog._appChooser.get_app_info();
addButton.sensitive = appInfo && this._checkId(appInfo.get_id());
_getRuleRows() {
return [...this._list].filter(row => !!row.id);
}
grid.attach(dialog._appChooser, 0, 0, 2, 1);
grid.attach(new Gtk.Label({ label: _("Workspace"),
halign: Gtk.Align.END }), 0, 1, 1, 1);
let adjustment = new Gtk.Adjustment({ lower: 1,
upper: WORKSPACE_MAX,
step_increment: 1
});
dialog._spin = new Gtk.SpinButton({ adjustment: adjustment,
snap_to_ticks: true });
dialog._spin.set_value(1);
grid.attach(dialog._spin, 1, 1, 1, 1);
dialog.get_content_area().add(grid);
_sync() {
const oldRules = this._getRuleRows();
const newRules = this._settings.get_strv(SETTINGS_KEY).map(entry => {
const [id, value] = entry.split(':');
return { id, value };
});
dialog.connect('response', Lang.bind(this, function(dialog, id) {
if (id != Gtk.ResponseType.OK) {
dialog.destroy();
return;
}
this._settings.block_signal_handler(this._changedId);
this._updateAction.enabled = false;
let appInfo = dialog._appChooser.get_app_info();
if (!appInfo)
return;
let index = Math.floor(dialog._spin.value);
if (isNaN(index) || index < 0)
index = 1;
newRules.forEach(({ id, value }, index) => {
const row = oldRules.find(r => r.id === id);
const appInfo = row
? null : Gio.DesktopAppInfo.new(id);
this._changedPermitted = false;
this._appendItem(appInfo.get_id(), index);
this._changedPermitted = true;
if (row)
row.set({ value });
else if (appInfo)
this._list.insert(new RuleRow(appInfo, value), index);
});
let iter = this._store.append();
let adj = new Gtk.Adjustment({ lower: 1,
upper: WORKSPACE_MAX,
step_increment: 1,
value: index });
this._store.set(iter,
[Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME, Columns.WORKSPACE, Columns.ADJUSTMENT],
[appInfo, appInfo.get_icon(), appInfo.get_display_name(), index, adj]);
const removed = oldRules.filter(
({ id }) => !newRules.find(r => r.id === id));
removed.forEach(r => this._list.remove(r));
this._settings.unblock_signal_handler(this._changedId);
this._updateAction.enabled = true;
}
});
const WorkspaceSelector = GObject.registerClass({
Properties: {
'number': GObject.ParamSpec.uint(
'number', 'number', 'number',
GObject.ParamFlags.READWRITE,
1, WORKSPACE_MAX, 1),
dialog.destroy();
}));
dialog.show_all();
},
}, class WorkspaceSelector extends Gtk.Widget {
static _classInit(klass) {
super._classInit(klass);
klass.set_layout_manager_type(Gtk.BoxLayout);
_deleteSelected: function() {
let [any, model, iter] = this._treeView.get_selection().get_selected();
return klass;
}
if (any) {
let appInfo = this._store.get_value(iter, Columns.APPINFO);
_init() {
super._init();
this.layout_manager.spacing = 6;
const label = new Gtk.Label({
xalign: 1,
margin_end: 6,
});
this.bind_property('number',
label, 'label',
GObject.BindingFlags.SYNC_CREATE);
label.set_parent(this);
const buttonProps = {
css_classes: ['circular'],
valign: Gtk.Align.CENTER,
};
this._decButton = new Gtk.Button({
icon_name: 'list-remove-symbolic',
...buttonProps,
});
this._decButton.set_parent(this);
this._decButton.connect('clicked', () => this.number--);
this._incButton = new Gtk.Button({
icon_name: 'list-add-symbolic',
...buttonProps,
});
this._incButton.set_parent(this);
this._incButton.connect('clicked', () => this.number++);
this.connect('notify::number', () => this._syncButtons());
this._syncButtons();
}
_syncButtons() {
this._decButton.sensitive = this.number > 1;
this._incButton.sensitive = this.number < WORKSPACE_MAX;
}
});
const RuleRow = GObject.registerClass({
Properties: {
'id': GObject.ParamSpec.string(
'id', 'id', 'id',
GObject.ParamFlags.READABLE,
''),
'value': GObject.ParamSpec.uint(
'value', 'value', 'value',
GObject.ParamFlags.READWRITE,
1, WORKSPACE_MAX, 1),
this._changedPermitted = false;
this._removeItem(appInfo.get_id());
this._changedPermitted = true;
this._store.remove(iter);
}
},
}, class RuleRow extends Adw.ActionRow {
_init(appInfo, value) {
super._init({
activatable: false,
title: appInfo.get_display_name(),
value,
});
this._appInfo = appInfo;
const icon = new Gtk.Image({
css_classes: ['icon-dropshadow'],
gicon: appInfo.get_icon(),
pixel_size: 32,
});
this.add_prefix(icon);
_workspaceEdited: function(renderer, pathString, text) {
let index = parseInt(text);
if (isNaN(index) || index < 0)
index = 1;
let path = Gtk.TreePath.new_from_string(pathString);
let [model, iter] = this._store.get_iter(path);
let appInfo = this._store.get_value(iter, Columns.APPINFO);
const wsButton = new WorkspaceSelector();
this.bind_property('value',
wsButton, 'number',
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL);
this.add_suffix(wsButton);
this._changedPermitted = false;
this._changeItem(appInfo.get_id(), index);
this._store.set_value(iter, Columns.WORKSPACE, index);
this._changedPermitted = true;
},
const button = new Gtk.Button({
action_name: 'rules.remove',
action_target: new GLib.Variant('s', this.id),
icon_name: 'edit-delete-symbolic',
has_frame: false,
valign: Gtk.Align.CENTER,
});
this.add_suffix(button);
_refresh: function() {
if (!this._changedPermitted)
// Ignore this notification, model is being modified outside
return;
this.connect('notify::value',
() => this.activate_action('rules.update', null));
}
this._store.clear();
get id() {
return this._appInfo.get_id();
let currentItems = this._settings.get_strv(SETTINGS_KEY);
let validItems = [ ];
for (let i = 0; i < currentItems.length; i++) {
let [id, index] = currentItems[i].split(':');
let appInfo = Gio.DesktopAppInfo.new(id);
if (!appInfo)
continue;
validItems.push(currentItems[i]);
let iter = this._store.append();
let adj = new Gtk.Adjustment({ lower: 1,
upper: WORKSPACE_MAX,
step_increment: 1,
value: index });
this._store.set(iter,
[Columns.APPINFO, Columns.ICON, Columns.DISPLAY_NAME, Columns.WORKSPACE, Columns.ADJUSTMENT],
[appInfo, appInfo.get_icon(), appInfo.get_display_name(), parseInt(index), adj]);
}
if (validItems.length != currentItems.length) // some items were filtered out
this._settings.set_strv(SETTINGS_KEY, validItems);
},
_checkId: function(id) {
let items = this._settings.get_strv(SETTINGS_KEY);
return !items.some(function(i) { return i.startsWith(id + ':'); });
},
_appendItem: function(id, workspace) {
let currentItems = this._settings.get_strv(SETTINGS_KEY);
currentItems.push(id + ':' + workspace);
this._settings.set_strv(SETTINGS_KEY, currentItems);
},
_removeItem: function(id) {
let currentItems = this._settings.get_strv(SETTINGS_KEY);
let index = currentItems.map(function(el) {
return el.split(':')[0];
}).indexOf(id);
if (index < 0)
return;
currentItems.splice(index, 1);
this._settings.set_strv(SETTINGS_KEY, currentItems);
},
_changeItem: function(id, workspace) {
let currentItems = this._settings.get_strv(SETTINGS_KEY);
let index = currentItems.map(function(el) {
return el.split(':')[0];
}).indexOf(id);
if (index < 0)
currentItems.push(id + ':' + workspace);
else
currentItems[index] = id + ':' + workspace;
this._settings.set_strv(SETTINGS_KEY, currentItems);
}
});
const NewRuleRow = GObject.registerClass(
class NewRuleRow extends Gtk.ListBoxRow {
_init() {
super._init({
action_name: 'rules.add',
child: new Gtk.Image({
icon_name: 'list-add-symbolic',
pixel_size: 16,
margin_top: 12,
margin_bottom: 12,
margin_start: 12,
margin_end: 12,
}),
});
this.update_property(
[Gtk.AccessibleProperty.LABEL], [_('Add Rule')]);
}
});
const NewRuleDialog = GObject.registerClass(
class NewRuleDialog extends Gtk.AppChooserDialog {
_init(parent) {
super._init({
transient_for: parent,
modal: true,
});
this._settings = ExtensionUtils.getSettings();
this.get_widget().set({
show_all: true,
show_other: true, // hide more button
});
this.get_widget().connect('application-selected',
this._updateSensitivity.bind(this));
this._updateSensitivity();
}
_updateSensitivity() {
const rules = this._settings.get_strv(SETTINGS_KEY);
const appInfo = this.get_widget().get_app_info();
this.set_response_sensitive(Gtk.ResponseType.OK,
appInfo && !rules.some(i => i.startsWith(appInfo.get_id())));
}
});
/** */
function init() {
ExtensionUtils.initTranslations();
Convenience.initTranslations();
}
/**
* @returns {Gtk.Widget} - the prefs widget
*/
function buildPrefsWidget() {
return new AutoMoveSettingsWidget();
let widget = new Widget({ margin: 12 });
widget.show_all();
return widget;
}

View File

@@ -0,0 +1,3 @@
EXTENSION_ID = drive-menu
include ../../extension.mk

View File

@@ -1,63 +1,56 @@
/* exported init enable disable */
// Drive menu extension
const { Clutter, Gio, GObject, Shell, St } = imports.gi;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const St = imports.gi.St;
const Shell = imports.gi.Shell;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
const Panel = imports.ui.panel;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const ShellMountOperation = imports.ui.shellMountOperation;
const _ = ExtensionUtils.gettext;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
Gio._promisify(Gio.File.prototype, 'query_filesystem_info_async');
const MountMenuItem = new Lang.Class({
Name: 'DriveMenu.MountMenuItem',
Extends: PopupMenu.PopupBaseMenuItem,
var MountMenuItem = GObject.registerClass(
class MountMenuItem extends PopupMenu.PopupBaseMenuItem {
_init(mount) {
super._init({
style_class: 'drive-menu-item',
});
_init: function(mount) {
this.parent();
this.label = new St.Label({
text: mount.get_name(),
x_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
this.add_child(this.label);
this.label_actor = this.label;
this.label = new St.Label({ text: mount.get_name() });
this.actor.add(this.label, { expand: true });
this.actor.label_actor = this.label;
this.mount = mount;
this.mount = mount;
this.connect('destroy', this._onDestroy.bind(this));
let ejectIcon = new St.Icon({ icon_name: 'media-eject-symbolic',
style_class: 'popup-menu-icon ' });
let ejectButton = new St.Button({ child: ejectIcon });
ejectButton.connect('clicked', Lang.bind(this, this._eject));
this.actor.add(ejectButton);
let ejectIcon = new St.Icon({
icon_name: 'media-eject-symbolic',
style_class: 'popup-menu-icon',
});
let ejectButton = new St.Button({
child: ejectIcon,
style_class: 'button',
});
ejectButton.connect('clicked', this._eject.bind(this));
this.add(ejectButton);
this.hide();
this._changedId = mount.connect('changed', this._syncVisibility.bind(this));
this._changedId = mount.connect('changed', Lang.bind(this, this._syncVisibility));
this._syncVisibility();
}
},
_onDestroy() {
destroy: function() {
if (this._changedId) {
this.mount.disconnect(this._changedId);
this._changedId = 0;
}
super.destroy();
}
this.parent();
},
async _isInteresting() {
_isInteresting: function() {
if (!this.mount.can_eject() && !this.mount.can_unmount())
return false;
if (this.mount.is_shadowed())
@@ -65,161 +58,151 @@ class MountMenuItem extends PopupMenu.PopupBaseMenuItem {
let volume = this.mount.get_volume();
if (volume)
return volume.get_identifier('class') !== 'network';
const root = this.mount.get_root();
try {
const attr = Gio.FILE_ATTRIBUTE_FILESYSTEM_REMOTE;
const info = await root.query_filesystem_info_async(attr, null);
return !info.get_attribute_boolean(attr);
} catch (e) {
log(`Failed to query filesystem: ${e.message}`);
if (volume == null) {
// probably a GDaemonMount, could be network or
// local, but we can't tell; assume it's local for now
return true;
}
// Hack, fall back to looking at GType
return Gio._LocalFilePrototype.isPrototypeOf(root);
}
return volume.get_identifier('class') != 'network';
},
async _syncVisibility() {
this.visible = await this._isInteresting();
}
_syncVisibility: function() {
this.actor.visible = this._isInteresting();
},
_eject() {
let unmountArgs = [
Gio.MountUnmountFlags.NONE,
new ShellMountOperation.ShellMountOperation(this.mount).mountOp,
null, // Gio.Cancellable
];
_eject: function() {
let mountOp = new ShellMountOperation.ShellMountOperation(this.mount);
if (this.mount.can_eject()) {
this.mount.eject_with_operation(...unmountArgs,
this._ejectFinish.bind(this));
} else {
this.mount.unmount_with_operation(...unmountArgs,
this._unmountFinish.bind(this));
}
}
if (this.mount.can_eject())
this.mount.eject_with_operation(Gio.MountUnmountFlags.NONE,
mountOp.mountOp,
null, // Gio.Cancellable
Lang.bind(this, this._ejectFinish));
else
this.mount.unmount_with_operation(Gio.MountUnmountFlags.NONE,
mountOp.mountOp,
null, // Gio.Cancellable
Lang.bind(this, this._unmountFinish));
},
_unmountFinish(mount, result) {
try {
mount.unmount_with_operation_finish(result);
} catch (e) {
this._reportFailure(e);
}
}
_unmountFinish: function(mount, result) {
try {
mount.unmount_with_operation_finish(result);
} catch(e) {
this._reportFailure(e);
}
},
_ejectFinish(mount, result) {
try {
mount.eject_with_operation_finish(result);
} catch (e) {
this._reportFailure(e);
}
}
_ejectFinish: function(mount, result) {
try {
mount.eject_with_operation_finish(result);
} catch(e) {
this._reportFailure(e);
}
},
_reportFailure(exception) {
// TRANSLATORS: %s is the filesystem name
let msg = _('Ejecting drive “%s” failed:').format(this.mount.get_name());
Main.notifyError(msg, exception.message);
}
_reportFailure: function(exception) {
let msg = _("Ejecting drive '%s' failed:").format(this.mount.get_name());
Main.notifyError(msg, exception.message);
},
activate(event) {
let uri = this.mount.get_root().get_uri();
activate: function(event) {
let context = global.create_app_launch_context(event.get_time(), -1);
Gio.AppInfo.launch_default_for_uri(uri, context);
Gio.AppInfo.launch_default_for_uri(this.mount.get_root().get_uri(),
context);
super.activate(event);
this.parent(event);
}
});
let DriveMenu = GObject.registerClass(
class DriveMenu extends PanelMenu.Button {
_init() {
super._init(0.0, _('Removable devices'));
const DriveMenu = new Lang.Class({
Name: 'DriveMenu.DriveMenu',
Extends: PanelMenu.Button,
let icon = new St.Icon({
icon_name: 'media-eject-symbolic',
style_class: 'system-status-icon',
});
_init: function() {
this.parent(0.0, _("Removable devices"));
this.add_child(icon);
let hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });
let icon = new St.Icon({ icon_name: 'media-eject-symbolic',
style_class: 'system-status-icon' });
this._monitor = Gio.VolumeMonitor.get();
this._addedId = this._monitor.connect('mount-added',
(monitor, mount) => this._addMount(mount));
this._removedId = this._monitor.connect('mount-removed', (monitor, mount) => {
this._removeMount(mount);
this._updateMenuVisibility();
});
hbox.add_child(icon);
hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
this.actor.add_child(hbox);
this._mounts = [];
this._monitor = Gio.VolumeMonitor.get();
this._addedId = this._monitor.connect('mount-added', Lang.bind(this, function(monitor, mount) {
this._addMount(mount);
this._updateMenuVisibility();
}));
this._removedId = this._monitor.connect('mount-removed', Lang.bind(this, function(monitor, mount) {
this._removeMount(mount);
this._updateMenuVisibility();
}));
this._monitor.get_mounts().forEach(this._addMount.bind(this));
this._mounts = [ ];
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this.menu.addAction(_('Open Files'), event => {
let appSystem = Shell.AppSystem.get_default();
let app = appSystem.lookup_app('org.gnome.Nautilus.desktop');
app.activate_full(-1, event.get_time());
});
this._monitor.get_mounts().forEach(Lang.bind(this, this._addMount));
this._updateMenuVisibility();
}
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
this.menu.addAction(_("Open File"), function(event) {
let appSystem = Shell.AppSystem.get_default();
let app = appSystem.lookup_app('org.gnome.Nautilus.desktop');
app.activate_full(-1, event.get_time());
});
_updateMenuVisibility() {
if (this._mounts.filter(i => i.visible).length > 0)
this.show();
else
this.hide();
}
this._updateMenuVisibility();
},
_addMount(mount) {
let item = new MountMenuItem(mount);
this._mounts.unshift(item);
this.menu.addMenuItem(item, 0);
_updateMenuVisibility: function() {
if (this._mounts.filter(function(i) i.actor.visible).length > 0)
this.actor.show();
else
this.actor.hide();
},
item.connect('notify::visible', () => this._updateMenuVisibility());
}
_addMount: function(mount) {
let item = new MountMenuItem(mount);
this._mounts.unshift(item);
this.menu.addMenuItem(item, 0);
},
_removeMount(mount) {
for (let i = 0; i < this._mounts.length; i++) {
let item = this._mounts[i];
if (item.mount === mount) {
item.destroy();
this._mounts.splice(i, 1);
return;
}
}
log('Removing a mount that was never added to the menu');
}
_removeMount: function(mount) {
for (let i = 0; i < this._mounts.length; i++) {
let item = this._mounts[i];
if (item.mount == mount) {
item.destroy();
this._mounts.splice(i, 1);
return;
}
}
log ('Removing a mount that was never added to the menu');
},
_onDestroy() {
if (this._addedId) {
this._monitor.disconnect(this._addedId);
this._monitor.disconnect(this._removedId);
this._addedId = 0;
this._removedId = 0;
destroy: function() {
if (this._connectedId) {
this._monitor.disconnect(this._connectedId);
this._monitor.disconnect(this._disconnectedId);
this._connectedId = 0;
this._disconnectedId = 0;
}
super._onDestroy();
}
this.parent();
},
});
/** */
function init() {
ExtensionUtils.initTranslations();
Convenience.initTranslations();
}
let _indicator;
/** */
function enable() {
_indicator = new DriveMenu();
_indicator = new DriveMenu;
Main.panel.addToStatusArea('drive-menu', _indicator);
}
/** */
function disable() {
_indicator.destroy();
}

View File

@@ -1,5 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)

View File

@@ -1,7 +1 @@
.drive-menu-item { spacing: 12px; }
.drive-menu-item .button {
border-radius: 99px;
padding: 3px;
min-height: auto;
}
/* This extensions requires no custom styling */

View File

@@ -0,0 +1,6 @@
EXTENSION_ID = example
EXTRA_MODULES = prefs.js
include ../../extension.mk
include ../../settings.mk

View File

@@ -0,0 +1,49 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
// Sample extension code, makes clicking on the panel show a message
const St = imports.gi.St;
const Mainloop = imports.mainloop;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const Main = imports.ui.main;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
function _showHello() {
let settings = Convenience.getSettings();
let text = settings.get_string('hello-text') || _("Hello, world!");
let label = new St.Label({ style_class: 'helloworld-label', text: text });
let monitor = Main.layoutManager.primaryMonitor;
global.stage.add_actor(label);
label.set_position(Math.floor (monitor.width / 2 - label.width / 2), Math.floor(monitor.height / 2 - label.height / 2));
Mainloop.timeout_add(3000, function () { label.destroy(); });
}
// Put your extension initialization code here
function init(metadata) {
log ('Example extension initalized');
Convenience.initTranslations();
}
let signalId;
function enable() {
log ('Example extension enabled');
Main.panel.actor.reactive = true;
signalId = Main.panel.actor.connect('button-release-event', _showHello);
}
function disable() {
log ('Example extension disabled');
if (signalId) {
Main.panel.actor.disconnect(signalId);
signalId = 0;
}
}

View File

@@ -0,0 +1,10 @@
{
"extension-id": "@extension_id@",
"uuid": "@uuid@",
"settings-schema": "@gschemaname@",
"gettext-domain": "@gettext_domain@",
"name": "Hello, World!",
"description": "An example extension to show how it works. Shows Hello, world when clicking on the top panel.",
"shell-version": [ "@shell_current@" ],
"url": "@url@"
}

View File

@@ -0,0 +1,9 @@
<schemalist gettext-domain="gnome-shell-extensions">
<schema id="org.gnome.shell.extensions.example" path="/org/gnome/shell/extensions/example/">
<key name="hello-text" type="s">
<default>''</default>
<summary>Alternative greeting text.</summary>
<description>If not empty, it contains the text that will be shown when clicking on the panel.</description>
</key>
</schema>
</schemalist>

View File

@@ -0,0 +1,57 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
function init() {
Convenience.initTranslations();
}
const ExamplePrefsWidget = new GObject.Class({
Name: 'Example.Prefs.Widget',
GTypeName: 'ExamplePrefsWidget',
Extends: Gtk.Grid,
_init: function(params) {
this.parent(params);
this.margin = 12;
this.row_spacing = this.column_spacing = 6;
this.set_orientation(Gtk.Orientation.VERTICAL);
this.add(new Gtk.Label({ label: '<b>' + _("Message") + '</b>',
use_markup: true,
halign: Gtk.Align.START }));
let entry = new Gtk.Entry({ hexpand: true,
margin_bottom: 12 });
this.add(entry);
this._settings = Convenience.getSettings();
this._settings.bind('hello-text', entry, 'text', Gio.SettingsBindFlags.DEFAULT);
// TRANSLATORS: Example is the name of the extension, should not be
// translated
let primaryText = _("Example aims to show how to build well behaved \
extensions for the Shell and as such it has little functionality on its own.\n\
Nevertheless it's possible to customize the greeting message.");
this.add(new Gtk.Label({ label: primaryText,
wrap: true, xalign: 0 }));
}
});
function buildPrefsWidget() {
let widget = new ExamplePrefsWidget();
widget.show_all();
return widget;
}

View File

@@ -0,0 +1,8 @@
/* Example stylesheet */
.helloworld-label {
font-size: 36px;
font-weight: bold;
color: #ffffff;
background-color: rgba(10,10,10,0.7);
border-radius: 5px;
}

View File

@@ -0,0 +1,3 @@
EXTENSION_ID = launch-new-instance
include ../../extension.mk

View File

@@ -1,17 +1,17 @@
/* exported enable disable */
const AppDisplay = imports.ui.appDisplay;
let _activateOriginal = null;
/** */
function enable() {
_activateOriginal = AppDisplay.AppIcon.prototype.activate;
AppDisplay.AppIcon.prototype.activate = function () {
_activateOriginal.call(this, 2);
};
function init() {
}
/** */
function disable() {
AppDisplay.AppIcon.prototype.activate = _activateOriginal;
function enable() {
_activateOriginal = AppDisplay.AppIcon.prototype.activate;
AppDisplay.AppIcon.prototype.activate = function() {
_activateOriginal.call(this, 2);
};
}
function disable() {
AppDisplay.AppIcon.prototype.activate = _activateOriginal;
}

View File

@@ -1,5 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)

View File

@@ -4,7 +4,7 @@
"settings-schema": "@gschemaname@",
"gettext-domain": "@gettext_domain@",
"name": "Launch new instance",
"description": "Always launch a new instance when clicking in the dash or the application view.\nThis extension is part of Classic Mode and is officially supported by GNOME. Please do not report bugs using the form below, use GNOME's GitLab instance instead.",
"description": "Always launch a new instance when clicking in the dash or the application view.\nThis extension is part of Classic Mode and is officially supported by GNOME. Please do not report bugs using the form below, use GNOME Bugzilla instead.",
"shell-version": [ "@shell_current@" ],
"url": "@url@"
}

View File

@@ -1,29 +0,0 @@
extension_schemas = []
js_sources = []
metadata_name = 'metadata.json'
foreach e : enabled_extensions
uuid = e + uuid_suffix
metadata_conf = configuration_data()
metadata_conf.set('extension_id', e)
metadata_conf.set('uuid', uuid)
metadata_conf.set('gschemaname', 'org.gnome.shell.extensions.' + e)
metadata_conf.set('gettext_domain', gettext_domain)
metadata_conf.set('shell_current', shell_version)
metadata_conf.set('url', 'https://gitlab.gnome.org/GNOME/gnome-shell-extensions')
extension_sources = files(e + '/extension.js')
extension_data = files(e + '/stylesheet.css')
subdir(e)
install_data (extension_sources + extension_data,
install_dir: join_paths(extensiondir, uuid)
)
endforeach
install_data (extension_schemas,
install_dir: schemadir
)

View File

@@ -1,8 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)
# extension_sources += files('prefs.js')
# extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')

View File

@@ -0,0 +1,5 @@
EXTENSION_ID = native-window-placement
include ../../extension.mk
include ../../settings.mk

View File

@@ -1,96 +1,149 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
/* exported enable disable */
const { Clutter } = imports.gi;
// import just everything from workspace.js:
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Meta = imports.gi.Meta;
const Pango = imports.gi.Pango;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Signals = imports.signals;
const DND = imports.ui.dnd;
const Lightbox = imports.ui.lightbox;
const Main = imports.ui.main;
const Overview = imports.ui.overview;
const Panel = imports.ui.panel;
const Tweener = imports.ui.tweener;
const Workspace = imports.ui.workspace;
const WindowPositionFlags = Workspace.WindowPositionFlags;
const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
const { WindowPreview } = imports.ui.windowPreview;
const Workspace = imports.ui.workspace;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
// testing settings for natural window placement strategy:
const WINDOW_PLACEMENT_NATURAL_FILLGAPS = true; // enlarge windows at the end to fill gaps // not implemented yet
const WINDOW_PLACEMENT_NATURAL_GRID_FALLBACK = true; // fallback to grid mode if all windows have the same size and positions. // not implemented yet
const WINDOW_PLACEMENT_NATURAL_ACCURACY = 20; // accuracy of window translate moves (KDE-default: 20)
const WINDOW_PLACEMENT_NATURAL_GAPS = 5; // half of the minimum gap between windows
const WINDOW_PLACEMENT_NATURAL_MAX_TRANSLATIONS = 5000; // safety limit for preventing endless loop if something is wrong in the algorithm
class Rect {
constructor(x, y, width, height) {
const PLACE_WINDOW_CAPTIONS_ON_TOP = true; // place window titles in overview on top of windows with overlap parameter
const WORKSPACE_BORDER_GAP = 10; // minimum gap between the workspace area and the workspace selector
const WINDOW_AREA_TOP_GAP = 20; // minimum gap between the workspace area and the top border. This keeps window captions and close buttons visible. 13px (26/2) should currently be enough.
const BUTTON_LAYOUT_SCHEMA = 'org.gnome.desktop.wm.preferences';
const BUTTON_LAYOUT_KEY = 'button-layout';
function injectToFunction(parent, name, func) {
let origin = parent[name];
parent[name] = function() {
let ret;
ret = origin.apply(this, arguments);
if (ret === undefined)
ret = func.apply(this, arguments);
return ret;
}
}
const Rect = new Lang.Class({
Name: 'NativeWindowPlacement.Rect',
_init: function(x, y, width, height) {
[this.x, this.y, this.width, this.height] = [x, y, width, height];
}
},
// used in _calculateWindowTransformationsNatural to replace Meta.Rectangle that is too slow.
copy() {
/**
* used in _calculateWindowTransformationsNatural to replace Meta.Rectangle that is too slow.
*/
copy: function() {
return new Rect(this.x, this.y, this.width, this.height);
}
},
union(rect2) {
union: function(rect2) {
let dest = this.copy();
if (rect2.x < dest.x) {
if (rect2.x < dest.x)
{
dest.width += dest.x - rect2.x;
dest.x = rect2.x;
}
if (rect2.y < dest.y) {
}
if (rect2.y < dest.y)
{
dest.height += dest.y - rect2.y;
dest.y = rect2.y;
}
}
if (rect2.x + rect2.width > dest.x + dest.width)
dest.width = rect2.x + rect2.width - dest.x;
dest.width = rect2.x + rect2.width - dest.x;
if (rect2.y + rect2.height > dest.y + dest.height)
dest.height = rect2.y + rect2.height - dest.y;
dest.height = rect2.y + rect2.height - dest.y;
return dest;
}
},
adjusted(dx, dy, dx2, dy2) {
adjusted: function(dx, dy, dx2, dy2) {
let dest = this.copy();
dest.x += dx;
dest.y += dy;
dest.width += -dx + dx2;
dest.height += -dy + dy2;
return dest;
}
},
overlap(rect2) {
return !(this.x + this.width <= rect2.x ||
rect2.x + rect2.width <= this.x ||
this.y + this.height <= rect2.y ||
rect2.y + rect2.height <= this.y);
}
overlap: function(rect2) {
return !((this.x + this.width <= rect2.x) ||
(rect2.x + rect2.width <= this.x) ||
(this.y + this.height <= rect2.y) ||
(rect2.y + rect2.height <= this.y));
},
center() {
center: function() {
return [this.x + this.width / 2, this.y + this.height / 2];
}
},
translate(dx, dy) {
translate: function(dx, dy) {
this.x += dx;
this.y += dy;
}
});
let winInjections, workspaceInjections, connectedSignals;
function resetState() {
winInjections = { };
workspaceInjections = { };
connectedSignals = [ ];
}
class NaturalLayoutStrategy extends Workspace.LayoutStrategy {
constructor(params, settings) {
super(params);
this._settings = settings;
}
function enable() {
resetState();
computeLayout(windows, _params) {
return {
windows,
};
}
let settings = Convenience.getSettings();
let useMoreScreen = settings.get_boolean('use-more-screen');
let windowCaptionsOnTop = settings.get_boolean('window-captions-on-top');
let signalId = settings.connect('changed::use-more-screen', function() {
useMoreScreen = settings.get_boolean('use-more-screen');
});
connectedSignals.push({ obj: settings, id: signalId });
/*
/**
* _calculateWindowTransformationsNatural:
* @clones: Array of #MetaWindow
*
* Returns clones with matching target coordinates and scales to arrange windows in a natural way that no overlap exists and relative window size is preserved.
* This function is almost a 1:1 copy of the function
* PresentWindowsEffect::calculateWindowTransformationsNatural() from KDE, see:
* https://projects.kde.org/projects/kde/kdebase/kde-workspace/repository/revisions/master/entry/kwin/effects/presentwindows/presentwindows.cpp
*/
computeWindowSlots(layout, area) {
Workspace.Workspace.prototype._calculateWindowTransformationsNatural = function(clones, area) {
// As we are using pseudo-random movement (See "slot") we need to make sure the list
// is always sorted the same way no matter which window is currently active.
let areaRect = new Rect(area.x, area.y, area.width, area.height);
let bounds = areaRect.copy();
let clones = layout.windows;
let area_rect = new Rect(area.x, area.y, area.width, area.height);
let bounds = area_rect.copy();
let direction = 0;
let directions = [];
@@ -104,36 +157,36 @@ class NaturalLayoutStrategy extends Workspace.LayoutStrategy {
// This is used when the window is on the edge of the screen to try to use as much screen real estate as possible.
directions[i] = direction;
direction++;
if (direction === 4)
if (direction == 4) {
direction = 0;
}
}
let loopCounter = 0;
let loop_counter = 0;
let overlap;
do {
overlap = false;
for (let i = 0; i < rects.length; i++) {
for (let j = 0; j < rects.length; j++) {
let adjustments = [-1, -1, 1, 1]
.map(v => (v *= WINDOW_PLACEMENT_NATURAL_GAPS));
let iAdjusted = rects[i].adjusted(...adjustments);
let jAdjusted = rects[j].adjusted(...adjustments);
if (i !== j && iAdjusted.overlap(jAdjusted)) {
loopCounter++;
if (i != j && rects[i].adjusted(-WINDOW_PLACEMENT_NATURAL_GAPS, -WINDOW_PLACEMENT_NATURAL_GAPS,
WINDOW_PLACEMENT_NATURAL_GAPS, WINDOW_PLACEMENT_NATURAL_GAPS).overlap(
rects[j].adjusted(-WINDOW_PLACEMENT_NATURAL_GAPS, -WINDOW_PLACEMENT_NATURAL_GAPS,
WINDOW_PLACEMENT_NATURAL_GAPS, WINDOW_PLACEMENT_NATURAL_GAPS))) {
loop_counter++;
overlap = true;
// TODO: something like a Point2D would be nicer here:
// Determine pushing direction
let iCenter = rects[i].center();
let jCenter = rects[j].center();
let diff = [jCenter[0] - iCenter[0], jCenter[1] - iCenter[1]];
let i_center = rects[i].center();
let j_center = rects[j].center();
let diff = [j_center[0] - i_center[0], j_center[1] - i_center[1]];
// Prevent dividing by zero and non-movement
if (diff[0] === 0 && diff[1] === 0)
if (diff[0] == 0 && diff[1] == 0)
diff[0] = 1;
// Try to keep screen/workspace aspect ratio
if (bounds.height / bounds.width > areaRect.height / areaRect.width)
if ( bounds.height / bounds.width > area_rect.height / area_rect.width )
diff[0] *= 2;
else
diff[1] *= 2;
@@ -148,7 +201,7 @@ class NaturalLayoutStrategy extends Workspace.LayoutStrategy {
rects[j].translate(diff[0], diff[1]);
if (this._settings.get_boolean('use-more-screen')) {
if (useMoreScreen) {
// Try to keep the bounding rect the same aspect as the screen so that more
// screen real estate is utilised. We do this by splitting the screen into nine
// equal sections, if the window center is in any of the corner sections pull the
@@ -162,33 +215,33 @@ class NaturalLayoutStrategy extends Workspace.LayoutStrategy {
let xSection = Math.round((rects[i].x - bounds.x) / (bounds.width / 3));
let ySection = Math.round((rects[i].y - bounds.y) / (bounds.height / 3));
iCenter = rects[i].center();
let i_center = rects[i].center();
diff[0] = 0;
diff[1] = 0;
if (xSection !== 1 || ySection !== 1) { // Remove this if you want the center to pull as well
if (xSection === 1)
xSection = directions[i] / 2 ? 2 : 0;
if (ySection === 1)
ySection = directions[i] % 2 ? 2 : 0;
if (xSection != 1 || ySection != 1) { // Remove this if you want the center to pull as well
if (xSection == 1)
xSection = (directions[i] / 2 ? 2 : 0);
if (ySection == 1)
ySection = (directions[i] % 2 ? 2 : 0);
}
if (xSection === 0 && ySection === 0) {
diff[0] = bounds.x - iCenter[0];
diff[1] = bounds.y - iCenter[1];
if (xSection == 0 && ySection == 0) {
diff[0] = bounds.x - i_center[0];
diff[1] = bounds.y - i_center[1];
}
if (xSection === 2 && ySection === 0) {
diff[0] = bounds.x + bounds.width - iCenter[0];
diff[1] = bounds.y - iCenter[1];
if (xSection == 2 && ySection == 0) {
diff[0] = bounds.x + bounds.width - i_center[0];
diff[1] = bounds.y - i_center[1];
}
if (xSection === 2 && ySection === 2) {
diff[0] = bounds.x + bounds.width - iCenter[0];
diff[1] = bounds.y + bounds.height - iCenter[1];
if (xSection == 2 && ySection == 2) {
diff[0] = bounds.x + bounds.width - i_center[0];
diff[1] = bounds.y + bounds.height - i_center[1];
}
if (xSection === 0 && ySection === 2) {
diff[0] = bounds.x - iCenter[0];
diff[1] = bounds.y + bounds.height - iCenter[1];
if (xSection == 0 && ySection == 2) {
diff[0] = bounds.x - i_center[0];
diff[1] = bounds.y + bounds.height - i_center[1];
}
if (diff[0] !== 0 || diff[1] !== 0) {
length = Math.sqrt(diff[0] * diff[0] + diff[1] * diff[1]);
if (diff[0] != 0 || diff[1] != 0) {
let length = Math.sqrt(diff[0]*diff[0] + diff[1]*diff[1]);
diff[0] *= WINDOW_PLACEMENT_NATURAL_ACCURACY / length / 2; // /2 to make it less influencing than the normal center-move above
diff[1] *= WINDOW_PLACEMENT_NATURAL_ACCURACY / length / 2;
rects[i].translate(diff[0], diff[1]);
@@ -201,94 +254,196 @@ class NaturalLayoutStrategy extends Workspace.LayoutStrategy {
}
}
}
} while (overlap && loopCounter < WINDOW_PLACEMENT_NATURAL_MAX_TRANSLATIONS);
} while (overlap && loop_counter < WINDOW_PLACEMENT_NATURAL_MAX_TRANSLATIONS);
// Work out scaling by getting the most top-left and most bottom-right window coords.
let scale;
scale = Math.min(
areaRect.width / bounds.width,
areaRect.height / bounds.height,
1.0);
scale = Math.min(area_rect.width / bounds.width,
area_rect.height / bounds.height,
1.0);
// Make bounding rect fill the screen size for later steps
bounds.x -= (areaRect.width - bounds.width * scale) / 2;
bounds.y -= (areaRect.height - bounds.height * scale) / 2;
bounds.width = areaRect.width / scale;
bounds.height = areaRect.height / scale;
bounds.x = bounds.x - (area_rect.width - bounds.width * scale) / 2;
bounds.y = bounds.y - (area_rect.height - bounds.height * scale) / 2;
bounds.width = area_rect.width / scale;
bounds.height = area_rect.height / scale;
// Move all windows back onto the screen and set their scale
for (let i = 0; i < rects.length; i++)
for (let i = 0; i < rects.length; i++) {
rects[i].translate(-bounds.x, -bounds.y);
}
// TODO: Implement the KDE part "Try to fill the gaps by enlarging windows if they have the space" here. (If this is wanted)
// rescale to workspace
let scales = [];
let buttonOuterHeight, captionHeight;
let buttonOuterWidth = 0;
let slots = [];
for (let i = 0; i < rects.length; i++) {
rects[i].x = rects[i].x * scale + areaRect.x;
rects[i].y = rects[i].y * scale + areaRect.y;
rects[i].width *= scale;
rects[i].height *= scale;
rects[i].x = rects[i].x * scale + area_rect.x;
rects[i].y = rects[i].y * scale + area_rect.y;
slots.push([rects[i].x, rects[i].y, rects[i].width, rects[i].height, clones[i]]);
slots.push([rects[i].x, rects[i].y, scale, clones[i]]);
}
return slots;
}
workspaceInjections['_calculateWindowTransformationsNatural'] = undefined;
/**
* _updateWindowPositions:
* @flags:
* INITIAL - this is the initial positioning of the windows.
* ANIMATE - Indicates that we need animate changing position.
*/
workspaceInjections['_updateWindowPositions'] = Workspace.Workspace.prototype._updateWindowPositions;
Workspace.Workspace.prototype._updateWindowPositions = function(flags) {
if (this._currentLayout == null) {
this._recalculateWindowPositions(flags);
return;
}
let initialPositioning = flags & WindowPositionFlags.INITIAL;
let animate = flags & WindowPositionFlags.ANIMATE;
let layout = this._currentLayout;
let strategy = layout.strategy;
let [, , padding] = this._getSpacingAndPadding();
let area = Workspace.padArea(this._actualGeometry, padding);
/// EDIT replace this version by our own:
//let slots = strategy.computeWindowSlots(layout, area);
/// EDIT copied from _realRecalculateWindowPositions:
let clones = this._windows.slice();
if (clones.length == 0)
return;
clones.sort(function(a, b) {
return a.metaWindow.get_stable_sequence() - b.metaWindow.get_stable_sequence();
});
if (this._reservedSlot)
clones.push(this._reservedSlot);
/// EDIT our own window placement function:
let slots = this._calculateWindowTransformationsNatural(clones, area);
let currentWorkspace = global.screen.get_active_workspace();
let isOnCurrentWorkspace = this.metaWorkspace == null || this.metaWorkspace == currentWorkspace;
for (let i = 0; i < slots.length; i++) {
let slot = slots[i];
let [x, y, scale, clone] = slot;
let metaWindow = clone.metaWindow;
let overlay = clone.overlay;
clone.slotId = i;
// Positioning a window currently being dragged must be avoided;
// we'll just leave a blank spot in the layout for it.
if (clone.inDrag)
continue;
let cloneWidth = clone.actor.width * scale;
let cloneHeight = clone.actor.height * scale;
clone.slot = [x, y, cloneWidth, cloneHeight];
if (overlay && (initialPositioning || !clone.positioned))
overlay.hide();
if (!clone.positioned) {
// This window appeared after the overview was already up
// Grow the clone from the center of the slot
clone.actor.x = x + cloneWidth / 2;
clone.actor.y = y + cloneHeight / 2;
clone.actor.scale_x = 0;
clone.actor.scale_y = 0;
clone.positioned = true;
}
if (animate && isOnCurrentWorkspace) {
if (!metaWindow.showing_on_its_workspace()) {
/* Hidden windows should fade in and grow
* therefore we need to resize them now so they
* can be scaled up later */
if (initialPositioning) {
clone.actor.opacity = 0;
clone.actor.scale_x = 0;
clone.actor.scale_y = 0;
clone.actor.x = x;
clone.actor.y = y;
}
Tweener.addTween(clone.actor,
{ opacity: 255,
time: Overview.ANIMATION_TIME,
transition: 'easeInQuad'
});
}
this._animateClone(clone, overlay, x, y, scale, initialPositioning);
} else {
// cancel any active tweens (otherwise they might override our changes)
Tweener.removeTweens(clone.actor);
clone.actor.set_position(x, y);
clone.actor.set_scale(scale, scale);
clone.overlay.relayout(false);
this._showWindowOverlay(clone, overlay, isOnCurrentWorkspace);
}
}
}
/// position window titles on top of windows in overlay ////
if (windowCaptionsOnTop) {
/// This is almost a direct copy of the original relayout function. Differences are marked.
winInjections['relayout'] = Workspace.WindowOverlay.prototype.relayout;
Workspace.WindowOverlay.prototype.relayout = function(animate) {
winInjections['relayout'].call(this, animate);
let title = this.title;
let border = this.border;
this._parentActor.set_child_above_sibling(title, border);
Tweener.removeTweens(title);
let [cloneX, cloneY, cloneWidth, cloneHeight] = this._windowClone.slot;
// Clutter.Actor.get_preferred_width() will return the fixed width if one
// is set, so we need to reset the width by calling set_width(-1), to forward
// the call down to StLabel.
// We also need to save and restore the current width, otherwise the animation
// starts from the wrong point.
let prevTitleWidth = title.width;
title.set_width(-1);
let [titleMinWidth, titleNatWidth] = title.get_preferred_width(-1);
let titleWidth = Math.max(titleMinWidth, Math.min(titleNatWidth, cloneWidth));
title.width = prevTitleWidth;
let titleX = cloneX + (cloneWidth - titleWidth) / 2;
/// this is the actual difference to original gnome-shell:
//let titleY = cloneY + cloneHeight + title._spacing;
let titleY = cloneY - title.height + title._spacing;
if (animate)
this._animateOverlayActor(title, Math.floor(titleX), Math.floor(titleY), titleWidth);
else {
title.width = titleWidth;
title.set_position(Math.floor(titleX), Math.floor(titleY));
}
};
}
}
let winInjections, workspaceInjections;
/** */
function resetState() {
winInjections = { };
workspaceInjections = { };
}
/** */
function enable() {
resetState();
let settings = ExtensionUtils.getSettings();
workspaceInjections['_createBestLayout'] = Workspace.WorkspaceLayout.prototype._createBestLayout;
Workspace.WorkspaceLayout.prototype._createBestLayout = function (_area) {
this._layoutStrategy = new NaturalLayoutStrategy({
monitor: Main.layoutManager.monitors[this._monitorIndex],
}, settings);
return this._layoutStrategy.computeLayout(this._sortedWindows);
};
// position window titles on top of windows in overlay
winInjections['_init'] = WindowPreview.prototype._init;
WindowPreview.prototype._init = function (...args) {
winInjections['_init'].call(this, ...args);
if (!settings.get_boolean('window-captions-on-top'))
return;
const alignConstraint = this._title.get_constraints().find(
c => c.align_axis && c.align_axis === Clutter.AlignAxis.Y_AXIS);
alignConstraint.factor = 0;
const bindConstraint = this._title.get_constraints().find(
c => c.coordinate && c.coordinate === Clutter.BindCoordinate.Y);
bindConstraint.offset = 0;
};
winInjections['_adjustOverlayOffsets'] =
WindowPreview.prototype._adjustOverlayOffsets;
WindowPreview.prototype._adjustOverlayOffsets = function (...args) {
winInjections['_adjustOverlayOffsets'].call(this, ...args);
if (settings.get_boolean('window-captions-on-top'))
this._title.translation_y = -this._title.translation_y;
};
}
/**
* @param {Object} object - object that was modified
* @param {Object} injection - the map of previous injections
* @param {string} name - the @injection key that should be removed
*/
function removeInjection(object, injection, name) {
if (injection[name] === undefined)
delete object[name];
@@ -296,15 +451,21 @@ function removeInjection(object, injection, name) {
object[name] = injection[name];
}
/** */
function disable() {
var i;
for (i in workspaceInjections)
removeInjection(Workspace.WorkspaceLayout.prototype, workspaceInjections, i);
removeInjection(Workspace.Workspace.prototype, workspaceInjections, i);
for (i in winInjections)
removeInjection(WindowPreview.prototype, winInjections, i);
removeInjection(Workspace.WindowOverlay.prototype, winInjections, i);
for each (i in connectedSignals)
i.obj.disconnect(i.id);
global.stage.queue_relayout();
resetState();
}
function init() {
/* do nothing */
}

View File

@@ -1,7 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)
extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')

View File

@@ -1 +1,9 @@
/* This extensions requires no special styling */
.window-caption {
-shell-caption-spacing: 13px; /* current caption height is 26px => set it to half of it. TODO: better solution needed */
}
.window-picker {
-horizontal-spacing: 32px;
-vertical-spacing: 32px;
padding: 64px 32px;
}

View File

@@ -0,0 +1,5 @@
EXTENSION_ID = places-menu
EXTRA_MODULES = placeDisplay.js
include ../../extension.mk

View File

@@ -1,155 +1,143 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
/* exported init enable disable */
const { Clutter, GObject, St } = imports.gi;
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const Lang = imports.lang;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Panel = imports.ui.panel;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const N_ = function(x) { return x; }
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
const PlaceDisplay = Me.imports.placeDisplay;
const _ = ExtensionUtils.gettext;
const N_ = x => x;
const PLACE_ICON_SIZE = 16;
var PlaceMenuItem = GObject.registerClass(
class PlaceMenuItem extends PopupMenu.PopupBaseMenuItem {
_init(info) {
super._init({
style_class: 'place-menu-item',
});
this._info = info;
const PlaceMenuItem = new Lang.Class({
Name: 'PlaceMenuItem',
Extends: PopupMenu.PopupBaseMenuItem,
this._icon = new St.Icon({
gicon: info.icon,
icon_size: PLACE_ICON_SIZE,
});
this.add_child(this._icon);
_init: function(info) {
this.parent();
this._info = info;
this._label = new St.Label({
text: info.name,
x_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
this.add_child(this._label);
this._icon = new St.Icon({ gicon: info.icon,
icon_size: PLACE_ICON_SIZE });
this.actor.add_child(this._icon);
if (info.isRemovable()) {
this._ejectIcon = new St.Icon({
icon_name: 'media-eject-symbolic',
style_class: 'popup-menu-icon',
});
this._ejectButton = new St.Button({
child: this._ejectIcon,
style_class: 'button',
});
this._ejectButton.connect('clicked', info.eject.bind(info));
this.add_child(this._ejectButton);
}
this._label = new St.Label({ text: info.name });
this.actor.add_child(this._label);
this._changedId = info.connect('changed',
this._propertiesChanged.bind(this));
}
Lang.bind(this, this._propertiesChanged));
},
destroy() {
destroy: function() {
if (this._changedId) {
this._info.disconnect(this._changedId);
this._changedId = 0;
}
super.destroy();
}
this.parent();
},
activate(event) {
this._info.launch(event.get_time());
activate: function(event) {
this._info.launch(event.get_time());
super.activate(event);
}
this.parent(event);
},
_propertiesChanged(info) {
_propertiesChanged: function(info) {
this._icon.gicon = info.icon;
this._label.text = info.name;
}
},
});
const SECTIONS = [
'special',
'devices',
'bookmarks',
'network',
];
'network'
]
let PlacesMenu = GObject.registerClass(
class PlacesMenu extends PanelMenu.Button {
_init() {
super._init(0.0, _('Places'));
const PlacesMenu = new Lang.Class({
Name: 'PlacesMenu.PlacesMenu',
Extends: PanelMenu.Button,
let label = new St.Label({
text: _('Places'),
y_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
this.add_actor(label);
_init: function() {
this.parent(0.0, _("Places"));
let hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });
let label = new St.Label({ text: _("Places"),
y_expand: true,
y_align: Clutter.ActorAlign.CENTER });
hbox.add_child(label);
hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
this.actor.add_actor(hbox);
this.placesManager = new PlaceDisplay.PlacesManager();
this._sections = { };
this._sections = { };
for (let i = 0; i < SECTIONS.length; i++) {
let id = SECTIONS[i];
this._sections[id] = new PopupMenu.PopupMenuSection();
this.placesManager.connect(`${id}-updated`, () => {
this._redisplay(id);
});
for (let i=0; i < SECTIONS.length; i++) {
let id = SECTIONS[i];
this._sections[id] = new PopupMenu.PopupMenuSection();
this.placesManager.connect(id + '-updated', Lang.bind(this, function() {
this._redisplay(id);
}));
this._create(id);
this.menu.addMenuItem(this._sections[id]);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
}
}
this._create(id);
this.menu.addMenuItem(this._sections[id]);
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
}
},
_onDestroy() {
this.placesManager.destroy();
destroy: function() {
this.placesManager.destroy();
super._onDestroy();
}
this.parent();
},
_redisplay(id) {
this._sections[id].removeAll();
_redisplay: function(id) {
this._sections[id].removeAll();
this._create(id);
}
},
_create(id) {
_create: function(id) {
let places = this.placesManager.get(id);
for (let i = 0; i < places.length; i++)
this._sections[id].addMenuItem(new PlaceMenuItem(places[i]));
this._sections[id].actor.visible = places.length > 0;
this._sections[id].actor.visible = places.length > 0;
}
});
/** */
function init() {
ExtensionUtils.initTranslations();
Convenience.initTranslations();
}
let _indicator;
/** */
function enable() {
_indicator = new PlacesMenu();
_indicator = new PlacesMenu;
let pos = Main.sessionMode.panel.left.indexOf('appMenu');
let pos = 1;
if ('apps-menu' in Main.panel.statusArea)
pos++;
pos = 2;
Main.panel.addToStatusArea('places-menu', _indicator, pos, 'left');
}
/** */
function disable() {
_indicator.destroy();
}

View File

@@ -1,7 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)
extension_sources += files('placeDisplay.js')

View File

@@ -4,7 +4,7 @@
"settings-schema": "@gschemaname@",
"gettext-domain": "@gettext_domain@",
"name": "Places Status Indicator",
"description": "Add a menu for quickly navigating places in the system.\nThis extension is part of Classic Mode and is officially supported by GNOME. Please do not report bugs using the form below, use GNOME's GitLab instance instead.",
"description": "Add a menu for quickly navigating places in the system.\nThis extension is part of Classic Mode and is officially supported by GNOME. Please do not report bugs using the form below, use GNOME Bugzilla instead.",
"shell-version": [ "@shell_current@" ],
"url": "@url@"
}

View File

@@ -1,17 +1,22 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const { Gio, GLib, Shell } = imports.gi;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Shell = imports.gi.Shell;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Signals = imports.signals;
const St = imports.gi.St;
const ExtensionUtils = imports.misc.extensionUtils;
const DND = imports.ui.dnd;
const Main = imports.ui.main;
const ShellMountOperation = imports.ui.shellMountOperation;
const Params = imports.misc.params;
const Search = imports.ui.search;
const Util = imports.misc.util;
const _ = ExtensionUtils.gettext;
const N_ = x => x;
Gio._promisify(Gio.AppInfo, 'launch_default_for_uri_async');
Gio._promisify(Gio.File.prototype, 'mount_enclosing_volume');
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const N_ = function(x) { return x; }
const BACKGROUND_SCHEMA = 'org.gnome.desktop.background';
@@ -22,221 +27,154 @@ const Hostname1Iface = '<node> \
</node>';
const Hostname1 = Gio.DBusProxy.makeProxyWrapper(Hostname1Iface);
class PlaceInfo {
constructor(...params) {
this._init(...params);
}
const PlaceInfo = new Lang.Class({
Name: 'PlaceInfo',
_init(kind, file, name, icon) {
_init: function(kind, file, name, icon) {
this.kind = kind;
this.file = file;
this.name = name || this._getFileName();
this.icon = icon ? new Gio.ThemedIcon({ name: icon }) : this.getIcon();
}
},
destroy() {
}
destroy: function() {
},
isRemovable() {
isRemovable: function() {
return false;
}
},
async _ensureMountAndLaunch(context, tryMount) {
try {
await Gio.AppInfo.launch_default_for_uri_async(this.file.get_uri(), context, null);
} catch (err) {
if (!err.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) {
Main.notifyError(_('Failed to launch “%s”').format(this.name), err.message);
return;
}
let source = {
get_icon: () => this.icon,
};
let op = new ShellMountOperation.ShellMountOperation(source);
try {
await this.file.mount_enclosing_volume(0, op.mountOp, null);
if (tryMount)
this._ensureMountAndLaunch(context, false);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED_HANDLED))
Main.notifyError(_('Failed to mount volume for “%s”').format(this.name), e.message);
} finally {
op.close();
}
}
}
launch(timestamp) {
launch: function(timestamp) {
let launchContext = global.create_app_launch_context(timestamp, -1);
this._ensureMountAndLaunch(launchContext, true);
}
getIcon() {
this.file.query_info_async('standard::symbolic-icon',
Gio.FileQueryInfoFlags.NONE,
0,
null,
(file, result) => {
try {
let info = file.query_info_finish(result);
this.icon = info.get_symbolic_icon();
this.emit('changed');
} catch (e) {
if (e instanceof Gio.IOErrorEnum)
return;
throw e;
}
try {
Gio.AppInfo.launch_default_for_uri(this.file.get_uri(),
launchContext);
} catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED)) {
this.file.mount_enclosing_volume(0, null, null, function(file, result) {
file.mount_enclosing_volume_finish(result);
Gio.AppInfo.launch_default_for_uri(file.get_uri(), launchContext);
});
// return a generic icon for this kind for now, until we have the
// icon from the query info above
switch (this.kind) {
case 'network':
return new Gio.ThemedIcon({ name: 'folder-remote-symbolic' });
case 'devices':
return new Gio.ThemedIcon({ name: 'drive-harddisk-symbolic' });
case 'special':
case 'bookmarks':
default:
if (!this.file.is_native())
return new Gio.ThemedIcon({ name: 'folder-remote-symbolic' });
else
return new Gio.ThemedIcon({ name: 'folder-symbolic' });
} catch(e) {
Main.notifyError(_("Failed to launch \"%s\"").format(this.name), e.message);
}
}
},
_getFileName() {
getIcon: function() {
try {
let info = this.file.query_info('standard::symbolic-icon', 0, null);
return info.get_symbolic_icon();
} catch(e if e instanceof Gio.IOErrorEnum) {
// return a generic icon for this kind
switch (this.kind) {
case 'network':
return new Gio.ThemedIcon({ name: 'folder-remote-symbolic' });
case 'devices':
return new Gio.ThemedIcon({ name: 'drive-harddisk-symbolic' });
case 'special':
case 'bookmarks':
default:
if (!this.file.is_native())
return new Gio.ThemedIcon({ name: 'folder-remote-symbolic' });
else
return new Gio.ThemedIcon({ name: 'folder-symbolic' });
}
}
},
_getFileName: function() {
try {
let info = this.file.query_info('standard::display-name', 0, null);
return info.get_display_name();
} catch (e) {
if (e instanceof Gio.IOErrorEnum)
return this.file.get_basename();
throw e;
} catch(e if e instanceof Gio.IOErrorEnum) {
return this.file.get_basename();
}
}
}
},
});
Signals.addSignalMethods(PlaceInfo.prototype);
class RootInfo extends PlaceInfo {
_init() {
super._init('devices', Gio.File.new_for_path('/'), _('Computer'));
const RootInfo = new Lang.Class({
Name: 'RootInfo',
Extends: PlaceInfo,
let busName = 'org.freedesktop.hostname1';
let objPath = '/org/freedesktop/hostname1';
new Hostname1(Gio.DBus.system, busName, objPath, (obj, error) => {
if (error)
return;
_init: function() {
this.parent('devices', Gio.File.new_for_path('/'), _("Computer"));
this._proxy = obj;
this._proxy.connect('g-properties-changed',
this._propertiesChanged.bind(this));
this._propertiesChanged(obj);
});
}
this._proxy = new Hostname1(Gio.DBus.system,
'org.freedesktop.hostname1',
'/org/freedesktop/hostname1',
Lang.bind(this, function(obj, error) {
if (error)
return;
getIcon() {
this._proxy.connect('g-properties-changed',
Lang.bind(this, this._propertiesChanged));
this._propertiesChanged(obj);
}));
},
getIcon: function() {
return new Gio.ThemedIcon({ name: 'drive-harddisk-symbolic' });
}
},
_propertiesChanged(proxy) {
_propertiesChanged: function(proxy) {
// GDBusProxy will emit a g-properties-changed when hostname1 goes down
// ignore it
if (proxy.g_name_owner) {
this.name = proxy.PrettyHostname || _('Computer');
this.name = proxy.PrettyHostname || _("Computer");
this.emit('changed');
}
},
destroy: function() {
this._proxy.run_dispose();
this.parent();
}
destroy() {
if (this._proxy) {
this._proxy.run_dispose();
this._proxy = null;
}
super.destroy();
}
}
});
class PlaceDeviceInfo extends PlaceInfo {
_init(kind, mount) {
const PlaceDeviceInfo = new Lang.Class({
Name: 'PlaceDeviceInfo',
Extends: PlaceInfo,
_init: function(kind, mount) {
this._mount = mount;
super._init(kind, mount.get_root(), mount.get_name());
}
this.parent(kind, mount.get_root(), mount.get_name());
},
getIcon() {
getIcon: function() {
return this._mount.get_symbolic_icon();
}
});
isRemovable() {
return this._mount.can_eject() || this._mount.can_unmount();
}
const PlaceVolumeInfo = new Lang.Class({
Name: 'PlaceVolumeInfo',
Extends: PlaceInfo,
eject() {
let unmountArgs = [
Gio.MountUnmountFlags.NONE,
new ShellMountOperation.ShellMountOperation(this._mount).mountOp,
null, // Gio.Cancellable
];
if (this._mount.can_eject()) {
this._mount.eject_with_operation(...unmountArgs,
this._ejectFinish.bind(this));
} else {
this._mount.unmount_with_operation(...unmountArgs,
this._unmountFinish.bind(this));
}
}
_ejectFinish(mount, result) {
try {
mount.eject_with_operation_finish(result);
} catch (e) {
this._reportFailure(e);
}
}
_unmountFinish(mount, result) {
try {
mount.unmount_with_operation_finish(result);
} catch (e) {
this._reportFailure(e);
}
}
_reportFailure(exception) {
let msg = _('Ejecting drive “%s” failed:').format(this._mount.get_name());
Main.notifyError(msg, exception.message);
}
}
class PlaceVolumeInfo extends PlaceInfo {
_init(kind, volume) {
_init: function(kind, volume) {
this._volume = volume;
super._init(kind, volume.get_activation_root(), volume.get_name());
}
this.parent(kind, volume.get_activation_root(), volume.get_name());
},
launch(timestamp) {
launch: function(timestamp) {
if (this.file) {
super.launch(timestamp);
this.parent(timestamp);
return;
}
this._volume.mount(0, null, null, (volume, result) => {
this._volume.mount(0, null, null, Lang.bind(this, function(volume, result) {
volume.mount_finish(result);
let mount = volume.get_mount();
this.file = mount.get_root();
super.launch(timestamp);
});
}
this.parent(timestamp);
}));
},
getIcon() {
getIcon: function() {
return this._volume.get_symbolic_icon();
}
}
});
const DEFAULT_DIRECTORIES = [
GLib.UserDirectory.DIRECTORY_DOCUMENTS,
@@ -246,8 +184,10 @@ const DEFAULT_DIRECTORIES = [
GLib.UserDirectory.DIRECTORY_VIDEOS,
];
var PlacesManager = class {
constructor() {
const PlacesManager = new Lang.Class({
Name: 'PlacesManager',
_init: function() {
this._places = {
special: [],
devices: [],
@@ -256,8 +196,9 @@ var PlacesManager = class {
};
this._settings = new Gio.Settings({ schema_id: BACKGROUND_SCHEMA });
this._showDesktopIconsChangedId = this._settings.connect(
'changed::show-desktop-icons', this._updateSpecials.bind(this));
this._showDesktopIconsChangedId =
this._settings.connect('changed::show-desktop-icons',
Lang.bind(this, this._updateSpecials));
this._updateSpecials();
/*
@@ -267,50 +208,41 @@ var PlacesManager = class {
this._connectVolumeMonitorSignals();
this._updateMounts();
this._bookmarksFile = this._findBookmarksFile();
this._bookmarksFile = this._findBookmarksFile()
this._bookmarkTimeoutId = 0;
this._monitor = null;
if (this._bookmarksFile) {
this._monitor = this._bookmarksFile.monitor_file(Gio.FileMonitorFlags.NONE, null);
this._monitor.connect('changed', () => {
this._monitor.connect('changed', Lang.bind(this, function () {
if (this._bookmarkTimeoutId > 0)
return;
/* Defensive event compression */
this._bookmarkTimeoutId = GLib.timeout_add(
GLib.PRIORITY_DEFAULT, 100, () => {
this._bookmarkTimeoutId = 0;
this._reloadBookmarks();
return false;
});
});
this._bookmarkTimeoutId = Mainloop.timeout_add(100, Lang.bind(this, function () {
this._bookmarkTimeoutId = 0;
this._reloadBookmarks();
return false;
}));
}));
this._reloadBookmarks();
}
}
},
_connectVolumeMonitorSignals() {
const signals = [
'volume-added',
'volume-removed',
'volume-changed',
'mount-added',
'mount-removed',
'mount-changed',
'drive-connected',
'drive-disconnected',
'drive-changed',
];
_connectVolumeMonitorSignals: function() {
const signals = ['volume-added', 'volume-removed', 'volume-changed',
'mount-added', 'mount-removed', 'mount-changed',
'drive-connected', 'drive-disconnected', 'drive-changed'];
this._volumeMonitorSignals = [];
let func = this._updateMounts.bind(this);
let func = Lang.bind(this, this._updateMounts);
for (let i = 0; i < signals.length; i++) {
let id = this._volumeMonitor.connect(signals[i], func);
this._volumeMonitorSignals.push(id);
}
}
},
destroy() {
destroy: function() {
if (this._settings)
this._settings.disconnect(this._showDesktopIconsChangedId);
this._settings = null;
@@ -321,19 +253,18 @@ var PlacesManager = class {
if (this._monitor)
this._monitor.cancel();
if (this._bookmarkTimeoutId)
GLib.source_remove(this._bookmarkTimeoutId);
}
Mainloop.source_remove(this._bookmarkTimeoutId);
},
_updateSpecials() {
this._places.special.forEach(p => p.destroy());
_updateSpecials: function() {
this._places.special.forEach(function (p) { p.destroy(); });
this._places.special = [];
let homePath = GLib.get_home_dir();
this._places.special.push(new PlaceInfo(
'special',
Gio.File.new_for_path(homePath),
_('Home')));
this._places.special.push(new PlaceInfo('special',
Gio.File.new_for_path(homePath),
_("Home")));
let specials = [];
let dirs = DEFAULT_DIRECTORIES.slice();
@@ -343,56 +274,55 @@ var PlacesManager = class {
for (let i = 0; i < dirs.length; i++) {
let specialPath = GLib.get_user_special_dir(dirs[i]);
if (!specialPath || specialPath === homePath)
if (specialPath == null || specialPath == homePath)
continue;
let file = Gio.File.new_for_path(specialPath), info;
try {
info = new PlaceInfo('special', file);
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
continue;
throw e;
} catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
continue;
}
specials.push(info);
}
specials.sort((a, b) => GLib.utf8_collate(a.name, b.name));
specials.sort(function(a, b) {
return GLib.utf8_collate(a.name, b.name);
});
this._places.special = this._places.special.concat(specials);
this.emit('special-updated');
}
},
_updateMounts() {
_updateMounts: function() {
let networkMounts = [];
let networkVolumes = [];
this._places.devices.forEach(p => p.destroy());
this._places.devices.forEach(function (p) { p.destroy(); });
this._places.devices = [];
this._places.network.forEach(p => p.destroy());
this._places.network.forEach(function (p) { p.destroy(); });
this._places.network = [];
/* Add standard places */
this._places.devices.push(new RootInfo());
this._places.network.push(new PlaceInfo(
'network',
Gio.File.new_for_uri('network:///'),
_('Browse Network'),
'network-workgroup-symbolic'));
this._places.network.push(new PlaceInfo('network',
Gio.File.new_for_uri('network:///'),
_("Browse Network"),
'network-workgroup-symbolic'));
/* first go through all connected drives */
let drives = this._volumeMonitor.get_connected_drives();
for (let i = 0; i < drives.length; i++) {
let volumes = drives[i].get_volumes();
for (let j = 0; j < volumes.length; j++) {
for(let j = 0; j < volumes.length; j++) {
let identifier = volumes[j].get_identifier('class');
if (identifier && identifier.includes('network')) {
if (identifier && identifier.indexOf('network') >= 0) {
networkVolumes.push(volumes[j]);
} else {
let mount = volumes[j].get_mount();
if (mount)
if(mount != null)
this._addMount('devices', mount);
}
}
@@ -400,27 +330,27 @@ var PlacesManager = class {
/* add all volumes that is not associated with a drive */
let volumes = this._volumeMonitor.get_volumes();
for (let i = 0; i < volumes.length; i++) {
if (volumes[i].get_drive())
for(let i = 0; i < volumes.length; i++) {
if(volumes[i].get_drive() != null)
continue;
let identifier = volumes[i].get_identifier('class');
if (identifier && identifier.includes('network')) {
if (identifier && identifier.indexOf('network') >= 0) {
networkVolumes.push(volumes[i]);
} else {
let mount = volumes[i].get_mount();
if (mount)
if(mount != null)
this._addMount('devices', mount);
}
}
/* add mounts that have no volume (/etc/mtab mounts, ftp, sftp,...) */
let mounts = this._volumeMonitor.get_mounts();
for (let i = 0; i < mounts.length; i++) {
if (mounts[i].is_shadowed())
for(let i = 0; i < mounts.length; i++) {
if(mounts[i].is_shadowed())
continue;
if (mounts[i].get_volume())
if(mounts[i].get_volume())
continue;
let root = mounts[i].get_default_location();
@@ -440,15 +370,15 @@ var PlacesManager = class {
this._addVolume('network', networkVolumes[i]);
}
for (let i = 0; i < networkMounts.length; i++)
for (let i = 0; i < networkMounts.length; i++) {
this._addMount('network', networkMounts[i]);
}
this.emit('devices-updated');
this.emit('network-updated');
}
},
_findBookmarksFile() {
_findBookmarksFile: function() {
let paths = [
GLib.build_filenamev([GLib.get_user_config_dir(), 'gtk-3.0', 'bookmarks']),
GLib.build_filenamev([GLib.get_home_dir(), '.gtk-bookmarks']),
@@ -460,9 +390,10 @@ var PlacesManager = class {
}
return null;
}
},
_reloadBookmarks: function() {
_reloadBookmarks() {
this._bookmarks = [];
let content = Shell.get_file_contents_utf8_sync(this._bookmarksFile.get_path());
@@ -472,7 +403,7 @@ var PlacesManager = class {
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let components = line.split(' ');
let [bookmark] = components;
let bookmark = components[0];
if (!bookmark)
continue;
@@ -482,16 +413,16 @@ var PlacesManager = class {
continue;
let duplicate = false;
for (let j = 0; j < this._places.special.length; j++) {
if (file.equal(this._places.special[j].file)) {
for (let i = 0; i < this._places.special.length; i++) {
if (file.equal(this._places.special[i].file)) {
duplicate = true;
break;
}
}
if (duplicate)
continue;
for (let j = 0; j < bookmarks.length; j++) {
if (file.equal(bookmarks[j].file)) {
for (let i = 0; i < bookmarks.length; i++) {
if (file.equal(bookmarks[i].file)) {
duplicate = true;
break;
}
@@ -509,38 +440,34 @@ var PlacesManager = class {
this._places.bookmarks = bookmarks;
this.emit('bookmarks-updated');
}
},
_addMount(kind, mount) {
_addMount: function(kind, mount) {
let devItem;
try {
devItem = new PlaceDeviceInfo(kind, mount);
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
return;
throw e;
} catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
return;
}
this._places[kind].push(devItem);
}
},
_addVolume(kind, volume) {
_addVolume: function(kind, volume) {
let volItem;
try {
volItem = new PlaceVolumeInfo(kind, volume);
} catch (e) {
if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
return;
throw e;
} catch(e if e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) {
return;
}
this._places[kind].push(volItem);
}
},
get(kind) {
get: function (kind) {
return this._places[kind];
}
};
});
Signals.addSignalMethods(PlacesManager.prototype);

View File

@@ -1,8 +1 @@
.place-menu-item .button {
border-radius: 99px;
padding: 3px;
min-height: auto;
}
.place-menu-item .button:ltr { margin-left: 6px; }
.place-menu-item .button:rtl { margin-right: 6px; }
/* none used*/

View File

@@ -0,0 +1,4 @@
EXTENSION_ID = screenshot-window-sizer
include ../../extension.mk
include ../../settings.mk

View File

@@ -1,4 +1,3 @@
/* exported enable disable */
/* Screenshot Window Sizer for Gnome Shell
*
* Copyright (c) 2013 Owen Taylor <otaylor@redhat.com>
@@ -19,46 +18,47 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
const { Clutter, Meta, Shell, St } = imports.gi;
const Gio = imports.gi.Gio;
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Main = imports.ui.main;
const Tweener = imports.ui.tweener;
const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
const MESSAGE_FADE_TIME = 2000;
const MESSAGE_FADE_TIME = 2;
let text;
let text, button;
/** */
function hideMessage() {
text.destroy();
text = null;
}
/**
* @param {string} message - the message to flash
*/
function flashMessage(message) {
if (!text) {
text = new St.Label({ style_class: 'screenshot-sizer-message' });
Main.uiGroup.add_actor(text);
}
text.remove_all_transitions();
Tweener.removeTweens(text);
text.text = message;
text.opacity = 255;
let monitor = Main.layoutManager.primaryMonitor;
text.set_position(
monitor.x + Math.floor(monitor.width / 2 - text.width / 2),
monitor.y + Math.floor(monitor.height / 2 - text.height / 2));
text.set_position(monitor.x + Math.floor(monitor.width / 2 - text.width / 2),
monitor.y + Math.floor(monitor.height / 2 - text.height / 2));
text.ease({
opacity: 0,
duration: MESSAGE_FADE_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: hideMessage,
});
Tweener.addTween(text,
{ opacity: 0,
time: MESSAGE_FADE_TIME,
transition: 'easeOutQuad',
onComplete: hideMessage });
}
let SIZES = [
@@ -66,33 +66,29 @@ let SIZES = [
[800, 450],
[1024, 576],
[1200, 675],
[1600, 900],
[360, 654], // Phone portrait maximized
[720, 360], // Phone landscape fullscreen
[1600, 900]
];
/**
* @param {Meta.Display} display - the display
* @param {Meta.Window=} window - for per-window bindings, the window
* @param {Meta.KeyBinding} binding - the key binding
*/
function cycleScreenshotSizes(display, window, binding) {
function cycleScreenshotSizes(display, screen, window, binding) {
// Probably this isn't useful with 5 sizes, but you can decrease instead
// of increase by holding down shift.
let modifiers = binding.get_modifiers();
let backwards = (modifiers & Meta.VirtualModifier.SHIFT_MASK) !== 0;
let backwards = (modifiers & Meta.VirtualModifier.SHIFT_MASK) != 0;
// Unmaximize first
if (window.get_maximized() !== 0)
window.unmaximize(Meta.MaximizeFlags.BOTH);
if (window.maximized_horizontally || window.maximizedVertically)
window.unmaximize(Meta.MaximizeFlags.HORIZONTAL | Meta.MaximizeFlags.VERTICAL);
let workArea = window.get_work_area_current_monitor();
let outerRect = window.get_frame_rect();
// Double both axes if on a hidpi display
let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
let scaledSizes = SIZES.map(size => size.map(wh => wh * scaleFactor))
.filter(([w, h]) => w <= workArea.width && h <= workArea.height);
let scaledSizes = SIZES.map(function(size) {
return size.map(function(wh) {
return wh * scaleFactor;
});
});
// Find the nearest 16:9 size for the current window size
let nearestIndex;
@@ -107,7 +103,7 @@ function cycleScreenshotSizes(display, window, binding) {
// get the best initial window size
let error = Math.abs(width - outerRect.width) + Math.abs(height - outerRect.height);
if (nearestIndex === undefined || error < nearestError) {
if (nearestIndex == null || error < nearestError) {
nearestIndex = i;
nearestError = error;
}
@@ -115,7 +111,10 @@ function cycleScreenshotSizes(display, window, binding) {
// get the next size up or down from ideal
let newIndex = (nearestIndex + (backwards ? -1 : 1)) % scaledSizes.length;
let [newWidth, newHeight] = scaledSizes[newIndex];
let newWidth, newHeight;
[newWidth, newHeight] = scaledSizes[newIndex];
if (newWidth > workArea.width || newHeight > workArea.height)
[newWidth, newHeight] = scaledSizes[0];
// Push the window onscreen if it would be resized offscreen
let newX = outerRect.x;
@@ -129,37 +128,30 @@ function cycleScreenshotSizes(display, window, binding) {
let newOuterRect = window.get_frame_rect();
let message = '%d×%d'.format(
newOuterRect.width / scaleFactor,
newOuterRect.height / scaleFactor);
(newOuterRect.width / scaleFactor),
(newOuterRect.height / scaleFactor));
// The new size might have been constrained by geometry hints (e.g. for
// a terminal) - in that case, include the actual ratio to the message
// we flash
let actualNumerator = 9 * newOuterRect.width / newOuterRect.height;
let actualNumerator = (newOuterRect.width / newOuterRect.height) * 9;
if (Math.abs(actualNumerator - 16) > 0.01)
message += ' (%.2f:9)'.format(actualNumerator);
flashMessage(message);
}
/** */
function enable() {
Main.wm.addKeybinding(
'cycle-screenshot-sizes',
ExtensionUtils.getSettings(),
Meta.KeyBindingFlags.PER_WINDOW,
Shell.ActionMode.NORMAL,
cycleScreenshotSizes);
Main.wm.addKeybinding(
'cycle-screenshot-sizes-backward',
ExtensionUtils.getSettings(),
Meta.KeyBindingFlags.PER_WINDOW | Meta.KeyBindingFlags.IS_REVERSED,
Shell.ActionMode.NORMAL,
cycleScreenshotSizes);
function init() {
}
function enable() {
Main.wm.addKeybinding('cycle-screenshot-sizes',
Convenience.getSettings(),
Meta.KeyBindingFlags.PER_WINDOW | Meta.KeyBindingFlags.REVERSES,
Shell.ActionMode.NORMAL,
cycleScreenshotSizes);
}
/** */
function disable() {
Main.wm.removeKeybinding('cycle-screenshot-sizes');
Main.wm.removeKeybinding('cycle-screenshot-sizes-backward');
}

View File

@@ -1,7 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)
extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')

View File

@@ -1,14 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="gnome-shell-extensions">
<schemalist>
<schema id="org.gnome.shell.extensions.screenshot-window-sizer"
path="/org/gnome/shell/extensions/screenshot-window-sizer/">
<key type="as" name="cycle-screenshot-sizes">
<default><![CDATA[['<Alt><Control>s']]]></default>
<summary>Cycle Screenshot Sizes</summary>
</key>
<key type="as" name="cycle-screenshot-sizes-backward">
<default><![CDATA[['<Shift><Alt><Control>s']]]></default>
<summary>Cycle Screenshot Sizes Backward</summary>
</key>
</schema>
</schemalist>

View File

@@ -0,0 +1,5 @@
EXTENSION_ID = user-theme
include ../../extension.mk
include ../../settings.mk

View File

@@ -1,61 +1,71 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
// Load shell theme from ~/.local/share/themes/name/gnome-shell
/* exported init */
// Load shell theme from ~/.themes/name/gnome-shell
const { Gio } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const Main = imports.ui.main;
const Me = ExtensionUtils.getCurrentExtension();
const Util = Me.imports.util;
const SETTINGS_KEY = 'name';
class ThemeManager {
enable() {
this._settings = ExtensionUtils.getSettings();
this._settings.connect(`changed::${SETTINGS_KEY}`, this._changeTheme.bind(this));
this._changeTheme();
}
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
disable() {
this._settings?.run_dispose();
this._settings = null;
const ThemeManager = new Lang.Class({
Name: 'UserTheme.ThemeManager',
_init: function() {
this._settings = Convenience.getSettings();
},
enable: function() {
this._changedId = this._settings.connect('changed::'+SETTINGS_KEY, Lang.bind(this, this._changeTheme));
this._changeTheme();
},
disable: function() {
if (this._changedId) {
this._settings.disconnect(this._changedId);
this._changedId = 0;
}
Main.setThemeStylesheet(null);
Main.loadTheme();
}
},
_changeTheme() {
let stylesheet = null;
let themeName = this._settings.get_string(SETTINGS_KEY);
_changeTheme: function() {
let _stylesheet = null;
let _themeName = this._settings.get_string(SETTINGS_KEY);
if (themeName) {
const stylesheetPaths = Util.getThemeDirs()
.map(dir => `${dir}/${themeName}/gnome-shell/gnome-shell.css`);
stylesheetPaths.push(...Util.getModeThemeDirs()
.map(dir => `${dir}/${themeName}.css`));
stylesheet = stylesheetPaths.find(path => {
let file = Gio.file_new_for_path(path);
return file.query_exists(null);
});
if (_themeName) {
let _userCssStylesheet = GLib.get_home_dir() + '/.themes/' + _themeName + '/gnome-shell/gnome-shell.css';
let file = Gio.file_new_for_path(_userCssStylesheet);
if (file.query_exists(null))
_stylesheet = _userCssStylesheet;
else {
let sysdirs = GLib.get_system_data_dirs();
sysdirs.unshift(GLib.get_user_data_dir());
for (let i = 0; i < sysdirs.length; i++) {
_userCssStylesheet = sysdirs[i] + '/themes/' + _themeName + '/gnome-shell/gnome-shell.css';
let file = Gio.file_new_for_path(_userCssStylesheet);
if (file.query_exists(null)) {
_stylesheet = _userCssStylesheet;
break;
}
}
}
}
if (stylesheet)
global.log(`loading user theme: ${stylesheet}`);
if (_stylesheet)
global.log('loading user theme: ' + _stylesheet);
else
global.log('loading default theme (Adwaita)');
Main.setThemeStylesheet(stylesheet);
Main.setThemeStylesheet(_stylesheet);
Main.loadTheme();
}
}
});
/**
* @returns {ThemeManager} - the extension state object
*/
function init() {
return new ThemeManager();
}

View File

@@ -1,8 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)
extension_sources += files('prefs.js', 'util.js')
extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')

View File

@@ -1,131 +0,0 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
/* exported init buildPrefsWidget */
// we use async/await here to not block the mainloop, not to parallelize
/* eslint-disable no-await-in-loop */
const { Adw, Gio, GLib, GObject, Gtk } = imports.gi;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Util = Me.imports.util;
Gio._promisify(Gio.File.prototype, 'enumerate_children_async');
Gio._promisify(Gio.File.prototype, 'query_info_async');
Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async');
const UserThemePrefsWidget = GObject.registerClass(
class UserThemePrefsWidget extends Adw.PreferencesGroup {
_init() {
super._init({ title: 'Themes' });
this._actionGroup = new Gio.SimpleActionGroup();
this.insert_action_group('theme', this._actionGroup);
this._settings = ExtensionUtils.getSettings();
this._actionGroup.add_action(
this._settings.create_action('name'));
this.connect('destroy', () => this._settings.run_dispose());
this._rows = new Map();
this._addTheme(''); // default
this._collectThemes();
}
async _collectThemes() {
for (const dirName of Util.getThemeDirs()) {
const dir = Gio.File.new_for_path(dirName);
for (const name of await this._enumerateDir(dir)) {
if (this._rows.has(name))
continue;
const file = dir.resolve_relative_path(
`${name}/gnome-shell/gnome-shell.css`);
try {
await file.query_info_async(
Gio.FILE_ATTRIBUTE_STANDARD_NAME,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT, null);
this._addTheme(name);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
logError(e);
}
}
}
for (const dirName of Util.getModeThemeDirs()) {
const dir = Gio.File.new_for_path(dirName);
for (const filename of await this._enumerateDir(dir)) {
if (!filename.endsWith('.css'))
continue;
const name = filename.slice(0, -4);
if (!this._rows.has(name))
this._addTheme(name);
}
}
}
_addTheme(name) {
const row = new ThemeRow(name);
this._rows.set(name, row);
this.add(row);
}
async _enumerateDir(dir) {
const fileInfos = [];
let fileEnum;
try {
fileEnum = await dir.enumerate_children_async(
Gio.FILE_ATTRIBUTE_STANDARD_NAME,
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT, null);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
logError(e);
return [];
}
let infos;
do {
infos = await fileEnum.next_files_async(100,
GLib.PRIORITY_DEFAULT, null);
fileInfos.push(...infos);
} while (infos.length > 0);
return fileInfos.map(info => info.get_name());
}
});
const ThemeRow = GObject.registerClass(
class ThemeRow extends Adw.ActionRow {
_init(name) {
const check = new Gtk.CheckButton({
action_name: 'theme.name',
action_target: new GLib.Variant('s', name),
});
super._init({
title: name || 'Default',
activatable_widget: check,
});
this.add_prefix(check);
}
});
/** */
function init() {
}
/**
* @returns {Gtk.Widget} - the prefs widget
*/
function buildPrefsWidget() {
return new UserThemePrefsWidget();
}

View File

@@ -1,23 +0,0 @@
/* exported getThemeDirs getModeThemeDirs */
const { GLib } = imports.gi;
const fn = (...args) => GLib.build_filenamev(args);
/**
* @returns {string[]} - an ordered list of theme directories
*/
function getThemeDirs() {
return [
fn(GLib.get_home_dir(), '.themes'),
fn(GLib.get_user_data_dir(), 'themes'),
...GLib.get_system_data_dirs().map(dir => fn(dir, 'themes')),
];
}
/**
* @returns {string[]} - an ordered list of mode theme directories
*/
function getModeThemeDirs() {
return GLib.get_system_data_dirs()
.map(dir => fn(dir, 'gnome-shell', 'theme'));
}

View File

@@ -0,0 +1,10 @@
EXTENSION_ID = window-list
EXTRA_MODULES = prefs.js
if CLASSIC_MODE
EXTRA_MODULES += classic.css
endif
include ../../extension.mk
include ../../settings.mk

View File

@@ -4,39 +4,36 @@
border-top-width: 1px;
border-bottom-width: 0px;
height: 2.25em ;
padding: 2px;
}
.bottom-panel .window-button > StWidget,
.bottom-panel .window-picker-toggle > StWidget {
color: #2e3436;
background-color: #eee;
border-radius: 3px;
padding: 3px 6px 1px;
box-shadow: none;
text-shadow: none;
border: 1px solid rgba(0,0,0,0.2);
}
.bottom-panel .window-button > StWidget {
background-gradient-drection: vertical;
background-color: #fff;
background-gradient-start: #fff;
background-gradient-end: #eee;
color: #000;
-st-natural-width: 18.7em;
max-width: 18.75em;
color: #2e3436;
background-color: #eee;
border-radius: 2px;
padding: 3px 6px 1px;
box-shadow: inset -1px -1px 1px rgba(0,0,0,0.5);
text-shadow: 0 0 transparent;
}
.bottom-panel .window-button:hover > StWidget,
.bottom-panel .window-picker-toggle:hover > StWidget {
.bottom-panel .window-button:hover > StWidget {
background-color: #f9f9f9;
}
.bottom-panel .window-button:active > StWidget,
.bottom-panel .window-button:focus > StWidget {
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
box-shadow: inset 1px 1px 2px rgba(0,0,0,0.5);
}
.bottom-panel .window-button.focused > StWidget,
.bottom-panel .window-picker-toggle:checked > StWidget {
background-color: #ccc;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
.bottom-panel .window-button.focused > StWidget {
background-color: #ddd;
box-shadow: inset 1px 1px 1px rgba(0,0,0,0.5);
}
.bottom-panel .window-button.focused:hover > StWidget {
@@ -45,24 +42,9 @@
.bottom-panel .window-button.minimized > StWidget {
color: #888;
box-shadow: none;
box-shadow: inset -1px -1px 1px rgba(0,0,0,0.5);
}
/* workspace switcher */
.window-list-workspace-indicator .workspace {
border: 2px solid #f6f5f4;
background-color: #ccc;
}
.window-list-workspace-indicator .workspace.active {
border-color: #888;
}
.window-list-window-preview {
background-color: #ededed;
border: 1px solid #ccc;
}
.window-list-window-preview.active {
background-color: #f6f5f4;
}
.bottom-panel .window-button.minimized > StWidget {
box-shadow: inset 1px 1px 1px rgba(0,0,0,0.5);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)
extension_sources += files('prefs.js', 'windowPicker.js', 'workspaceIndicator.js')
extension_schemas += files(metadata_conf.get('gschemaname') + '.gschema.xml')
if classic_mode_enabled
extension_data += files('classic.css')
endif

View File

@@ -4,7 +4,7 @@
"settings-schema": "@gschemaname@",
"gettext-domain": "@gettext_domain@",
"name": "Window List",
"description": "Display a window list at the bottom of the screen.\nThis extension is part of Classic Mode and is officially supported by GNOME. Please do not report bugs using the form below, use GNOME's GitLab instance instead.",
"description": "Display a window list at the bottom of the screen.\nThis extension is part of Classic Mode and is officially supported by GNOME. Please do not report bugs using the form below, use GNOME Bugzilla instead.",
"shell-version": [ "@shell_current@" ],
"url": "@url@"
}

View File

@@ -12,14 +12,7 @@
<summary>When to group windows</summary>
<description>
Decides when to group windows from the same application on the
window list. Possible values are never, auto and always.
</description>
</key>
<key name="display-all-workspaces" type="b">
<default>false</default>
<summary>Show windows from all workspaces</summary>
<description>
Whether to show windows from all workspaces or only the current one.
window list. Possible values are "never", "auto" and "always".
</description>
</key>
<key name="show-on-all-monitors" type="b">

View File

@@ -1,89 +1,87 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
/* exported init buildPrefsWidget */
const { Adw, Gio, GLib, GObject, Gtk } = imports.gi;
const Gio = imports.gi.Gio;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
const _ = ExtensionUtils.gettext;
/** */
function init() {
ExtensionUtils.initTranslations();
Convenience.initTranslations();
}
const WindowListPrefsWidget = GObject.registerClass(
class WindowListPrefsWidget extends Adw.PreferencesPage {
_init() {
super._init();
const WindowListPrefsWidget = new GObject.Class({
Name: 'WindowList.Prefs.Widget',
GTypeName: 'WindowListPrefsWidget',
Extends: Gtk.Grid,
this._actionGroup = new Gio.SimpleActionGroup();
this.insert_action_group('window-list', this._actionGroup);
_init: function(params) {
this.parent(params);
this._settings = ExtensionUtils.getSettings();
this._actionGroup.add_action(
this._settings.create_action('grouping-mode'));
this._actionGroup.add_action(
this._settings.create_action('show-on-all-monitors'));
this._actionGroup.add_action(
this._settings.create_action('display-all-workspaces'));
this.margin = 24;
this.row_spacing = 6;
this.orientation = Gtk.Orientation.VERTICAL;
const groupingGroup = new Adw.PreferencesGroup({
title: _('Window Grouping'),
});
this.add(groupingGroup);
let groupingLabel = '<b>' + _("Window Grouping") + '</b>';
this.add(new Gtk.Label({ label: groupingLabel, use_markup: true,
halign: Gtk.Align.START }));
const modes = [
{ mode: 'never', title: _('Never group windows') },
{ mode: 'auto', title: _('Group windows when space is limited') },
{ mode: 'always', title: _('Always group windows') },
];
let align = new Gtk.Alignment({ left_padding: 12 });
this.add(align);
for (const { mode, title } of modes) {
const check = new Gtk.CheckButton({
action_name: 'window-list.grouping-mode',
action_target: new GLib.Variant('s', mode),
});
const row = new Adw.ActionRow({
activatable_widget: check,
title,
});
row.add_prefix(check);
groupingGroup.add(row);
let grid = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL,
row_spacing: 6,
column_spacing: 6 });
align.add(grid);
this._settings = Convenience.getSettings();
let currentMode = this._settings.get_string('grouping-mode');
let range = this._settings.get_range('grouping-mode');
let modes = range.deep_unpack()[1].deep_unpack();
let modeLabels = {
'never': _("Never group windows"),
'auto': _("Group windows when space is limited"),
'always': _("Always group windows")
};
let radio = null;
for (let i = 0; i < modes.length; i++) {
let mode = modes[i];
let label = modeLabels[mode];
if (!label) {
log('Unhandled option "%s" for grouping-mode'.format(mode));
continue;
}
radio = new Gtk.RadioButton({ active: currentMode == mode,
label: label,
group: radio });
grid.add(radio);
radio.connect('toggled', Lang.bind(this, function(button) {
if (button.active)
this._settings.set_string('grouping-mode', mode);
}));
}
const miscGroup = new Adw.PreferencesGroup();
this.add(miscGroup);
let toggle = new Gtk.Switch({
action_name: 'window-list.show-on-all-monitors',
valign: Gtk.Align.CENTER,
});
let row = new Adw.ActionRow({
title: _('Show on all monitors'),
activatable_widget: toggle,
});
row.add_suffix(toggle);
miscGroup.add(row);
toggle = new Gtk.Switch({
action_name: 'window-list.display-all-workspaces',
valign: Gtk.Align.CENTER,
});
this._settings.bind('display-all-workspaces',
toggle, 'active', Gio.SettingsBindFlags.DEFAULT);
row = new Adw.ActionRow({
title: _('Show windows from all workspaces'),
activatable_widget: toggle,
});
row.add_suffix(toggle);
miscGroup.add(row);
let check = new Gtk.CheckButton({ label: _("Show on all monitors"),
margin_top: 6 });
this._settings.bind('show-on-all-monitors', check, 'active', Gio.SettingsBindFlags.DEFAULT);
this.add(check);
}
});
/**
* @returns {Gtk.Widget} - the prefs widget
*/
function buildPrefsWidget() {
return new WindowListPrefsWidget();
let widget = new WindowListPrefsWidget();
widget.show_all();
return widget;
}

View File

@@ -1,3 +1,10 @@
.bottom-panel {
/* .window-button-icon height +
.window-button vertical padding +
.window-button > StWidget vertical padding) */
height: 2.25em;
}
.window-list {
spacing: 2px;
font-size: 10pt;
@@ -19,31 +26,18 @@
spacing: 4px;
}
.window-button > StWidget,
.window-picker-toggle > StWidget {
.window-button > StWidget {
-st-natural-width: 18.75em;
max-width: 18.75em;
color: #bbb;
background-color: black;
border-radius: 2px;
border-radius: 4px;
padding: 3px 6px 1px;
box-shadow: inset 1px 1px 4px rgba(255,255,255,0.5);
text-shadow: 1px 1px 4px rgba(0,0,0,0.8);
}
.window-picker-toggle {
padding: 3px;
}
.window-picker-toggle > StWidet {
border: 1px solid rgba(255,255,255,0.3);
}
.window-button > StWidget {
-st-natural-width: 18.75em;
max-width: 18.75em;
}
.window-button:hover > StWidget,
.window-picker-toggle:hover > StWidget {
.window-button:hover > StWidget {
color: white;
background-color: #1f1f1f;
}
@@ -53,14 +47,12 @@
box-shadow: inset 2px 2px 4px rgba(255,255,255,0.5);
}
.window-button.focused > StWidget,
.window-picker-toggle:checked > StWidget {
.window-button.focused > StWidget {
color: white;
box-shadow: inset 1px 1px 4px rgba(255,255,255,0.7);
}
.window-button.focused:active > StWidget,
.window-picker-toggle:checked:active > StWidget {
.window-button.focused:active > StWidget {
box-shadow: inset 2px 2px 4px rgba(255,255,255,0.7);
}
@@ -78,36 +70,13 @@
height: 24px;
}
.window-list-workspace-indicator .status-label-bin {
background-color: rgba(200, 200, 200, .3);
border: 1px solid #cccccc;
padding: 0 3px;
margin: 3px;
}
.window-list-workspace-indicator .workspaces-box {
spacing: 3px;
.window-list-workspace-indicator {
padding: 3px;
}
.window-list-workspace-indicator .workspace {
border: 2px solid #000;
width: 52px;
border-radius: 4px;
background-color: #595959;
}
.window-list-workspace-indicator .workspace.active {
border-color: #fff;
}
.window-list-window-preview {
background-color: #bebebe;
border: 1px solid #828282;
}
.window-list-window-preview.active {
background-color: #d4d4d4;
.window-list-workspace-indicator > StWidget {
background-color: rgba(200, 200, 200, .3);
border: 1px solid #cccccc;
}
.notification {

View File

@@ -1,348 +0,0 @@
/* exported WindowPicker, WindowPickerToggle */
const { Clutter, GObject, Shell, St } = imports.gi;
const Layout = imports.ui.layout;
const Main = imports.ui.main;
const { WorkspacesDisplay } = imports.ui.workspacesView;
const Workspace = imports.ui.workspace;
const { VIGNETTE_BRIGHTNESS } = imports.ui.lightbox;
const {
SIDE_CONTROLS_ANIMATION_TIME,
OverviewAdjustment,
ControlsState,
} = imports.ui.overviewControls;
let MyWorkspacesDisplay = GObject.registerClass(
class MyWorkspacesDisplay extends WorkspacesDisplay {
_init(controls, overviewAdjustment) {
let workspaceManager = global.workspace_manager;
this._overviewAdjustment = overviewAdjustment;
this._workspaceAdjustment = new St.Adjustment({
value: workspaceManager.get_active_workspace_index(),
lower: 0,
page_increment: 1,
page_size: 1,
step_increment: 0,
upper: workspaceManager.n_workspaces,
});
this._nWorkspacesChangedId =
workspaceManager.connect('notify::n-workspaces',
this._updateAdjustment.bind(this));
super._init(controls, this._workspaceAdjustment, this._overviewAdjustment);
this._workspaceAdjustment.actor = this;
this.add_constraint(
new Layout.MonitorConstraint({
primary: true,
work_area: true,
}));
}
prepareToEnterOverview(...args) {
if (!this._scrollEventId) {
this._scrollEventId = Main.windowPicker.connect('scroll-event',
this._onScrollEvent.bind(this));
}
super.prepareToEnterOverview(...args);
}
vfunc_hide(...args) {
if (this._scrollEventId > 0)
Main.windowPicker.disconnect(this._scrollEventId);
this._scrollEventId = 0;
super.vfunc_hide(...args);
}
_updateAdjustment() {
let workspaceManager = global.workspace_manager;
this._workspaceAdjustment.set({
upper: workspaceManager.n_workspaces,
value: workspaceManager.get_active_workspace_index(),
});
}
_onDestroy() {
if (this._nWorkspacesChangedId)
global.workspace_manager.disconnect(this._nWorkspacesChangedId);
this._nWorkspacesChangedId = 0;
super._onDestroy();
}
});
const MyWorkspace = GObject.registerClass(
class MyWorkspace extends Workspace.Workspace {
_init(...args) {
super._init(...args);
this._adjChangedId =
this._overviewAdjustment.connect('notify::value', () => {
const { value: progress } = this._overviewAdjustment;
const brightness = 1 - (1 - VIGNETTE_BRIGHTNESS) * progress;
for (const bg of this._background?._backgroundGroup ?? []) {
bg.content.set({
vignette: true,
brightness,
});
}
});
}
_onDestroy() {
super._onDestroy();
if (this._adjChangedId)
this._overviewAdjustment.disconnect(this._adjChangedId);
this._adjChangedId = 0;
}
});
const MyWorkspaceBackground = GObject.registerClass(
class MyWorkspaceBackground extends Workspace.WorkspaceBackground {
_updateBorderRadius() {
}
vfunc_allocate(box) {
this.set_allocation(box);
const themeNode = this.get_theme_node();
const contentBox = themeNode.get_content_box(box);
this._bin.allocate(contentBox);
const [contentWidth, contentHeight] = contentBox.get_size();
const monitor = Main.layoutManager.monitors[this._monitorIndex];
const xRatio = contentWidth / this._workarea.width;
const yRatio = contentHeight / this._workarea.height;
const right = area => area.x + area.width;
const bottom = area => area.y + area.height;
const offsets = {
left: xRatio * (this._workarea.x - monitor.x),
right: xRatio * (right(monitor) - right(this._workarea)),
top: yRatio * (this._workarea.y - monitor.y),
bottom: yRatio * (bottom(monitor) - bottom(this._workarea)),
};
contentBox.set_origin(-offsets.left, -offsets.top);
contentBox.set_size(
offsets.left + contentWidth + offsets.right,
offsets.top + contentHeight + offsets.bottom);
this._backgroundGroup.allocate(contentBox);
}
});
var WindowPicker = GObject.registerClass({
Signals: {
'open-state-changed': { param_types: [GObject.TYPE_BOOLEAN] },
},
}, class extends Clutter.Actor {
_init() {
this._visible = false;
this._modal = false;
this._overlayKeyId = 0;
this._stageKeyPressId = 0;
super._init({ reactive: true });
this._adjustment = new OverviewAdjustment(this);
this.connect('destroy', this._onDestroy.bind(this));
global.bind_property('screen-width',
this, 'width',
GObject.BindingFlags.SYNC_CREATE);
global.bind_property('screen-height',
this, 'height',
GObject.BindingFlags.SYNC_CREATE);
this._workspacesDisplay = new MyWorkspacesDisplay(this, this._adjustment);
this.add_child(this._workspacesDisplay);
Main.uiGroup.insert_child_below(this, global.window_group);
if (!Main.sessionMode.hasOverview) {
this._injectBackgroundShade();
this._overlayKeyId = global.display.connect('overlay-key', () => {
if (!this._visible)
this.open();
else
this.close();
});
}
}
_injectBackgroundShade() {
this._origWorkspace = Workspace.Workspace;
this._origWorkspaceBackground = Workspace.WorkspaceBackground;
Workspace.Workspace = MyWorkspace;
Workspace.WorkspaceBackground = MyWorkspaceBackground;
}
get visible() {
return this._visible;
}
open() {
if (this._visible)
return;
this._visible = true;
if (!this._syncGrab())
return;
this._fakeOverviewVisible(true);
this._workspacesDisplay.prepareToEnterOverview();
Main.overview._animationInProgress = true;
this._adjustment.value = ControlsState.HIDDEN;
this._adjustment.ease(ControlsState.WINDOW_PICKER, {
duration: SIDE_CONTROLS_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => (Main.overview._animationInProgress = false),
});
this._stageKeyPressId = global.stage.connect('key-press-event',
(a, event) => {
let sym = event.get_key_symbol();
if (sym === Clutter.KEY_Escape) {
this.close();
return Clutter.EVENT_STOP;
}
return Clutter.EVENT_PROPAGATE;
});
this.emit('open-state-changed', this._visible);
}
close() {
if (!this._visible)
return;
this._visible = false;
if (!this._syncGrab())
return;
this._workspacesDisplay.prepareToLeaveOverview();
Main.overview._animationInProgress = true;
this._adjustment.ease(ControlsState.HIDDEN, {
duration: SIDE_CONTROLS_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
Main.overview._animationInProgress = false;
this._workspacesDisplay.hide();
this._fakeOverviewVisible(false);
},
});
global.stage.disconnect(this._stageKeyPressId);
this._stageKeyPressId = 0;
this.emit('open-state-changed', this._visible);
}
getWorkspacesBoxForState() {
return this.allocation;
}
_fakeOverviewVisible(visible) {
// Fake overview state for WorkspacesDisplay
Main.overview._visible = visible;
// Hide real windows
Main.layoutManager._inOverview = visible;
Main.layoutManager._updateVisibility();
}
_syncGrab() {
if (this._visible) {
if (this._modal)
return true;
const grab = Main.pushModal(global.stage, {
actionMode: Shell.ActionMode.OVERVIEW,
});
if (grab.get_seat_state() !== Clutter.GrabState.NONE) {
this._grab = grab;
this._modal = true;
} else {
Main.popModal(grab);
this.hide();
return false;
}
} else if (this._modal) {
Main.popModal(this._grab);
this._modal = false;
this._grab = null;
}
return true;
}
_onDestroy() {
if (this._origWorkspace)
Workspace.Workspace = this._origWorkspace;
if (this._origWorkspaceBackground)
Workspace.WorkspaceBackground = this._origWorkspaceBackground;
if (this._monitorsChangedId)
Main.layoutManager.disconnect(this._monitorsChangedId);
this._monitorsChangedId = 0;
if (this._overlayKeyId)
global.display.disconnect(this._overlayKeyId);
this._overlayKeyId = 0;
if (this._stageKeyPressId)
global.stage.disconnect(this._stageKeyPressId);
this._stageKeyPressId = 0;
}
});
var WindowPickerToggle = GObject.registerClass(
class WindowPickerToggle extends St.Button {
_init() {
let iconBin = new St.Widget({
layout_manager: new Clutter.BinLayout(),
});
iconBin.add_child(new St.Icon({
icon_name: 'focus-windows-symbolic',
icon_size: 16,
x_expand: true,
y_expand: true,
x_align: Clutter.ActorAlign.CENTER,
y_align: Clutter.ActorAlign.CENTER,
}));
super._init({
style_class: 'window-picker-toggle',
child: iconBin,
visible: !Main.sessionMode.hasOverview,
toggle_mode: true,
});
this.connect('notify::checked', () => {
if (this.checked)
Main.windowPicker.open();
else
Main.windowPicker.close();
});
Main.windowPicker.connect('open-state-changed', () => {
this.checked = Main.windowPicker.visible;
});
}
});

View File

@@ -1,439 +0,0 @@
/* exported WorkspaceIndicator */
const { Clutter, Gio, GObject, Meta, St } = imports.gi;
const DND = imports.ui.dnd;
const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const _ = ExtensionUtils.gettext;
const TOOLTIP_OFFSET = 6;
const TOOLTIP_ANIMATION_TIME = 150;
const MAX_THUMBNAILS = 6;
let WindowPreview = GObject.registerClass(
class WindowPreview extends St.Button {
_init(window) {
super._init({
style_class: 'window-list-window-preview',
});
this._delegate = this;
DND.makeDraggable(this, { restoreOnSuccess: true });
this._window = window;
this.connect('destroy', this._onDestroy.bind(this));
this._sizeChangedId = this._window.connect('size-changed',
() => this.queue_relayout());
this._positionChangedId = this._window.connect('position-changed',
() => {
this._updateVisible();
this.queue_relayout();
});
this._minimizedChangedId = this._window.connect('notify::minimized',
this._updateVisible.bind(this));
this._focusChangedId = global.display.connect('notify::focus-window',
this._onFocusChanged.bind(this));
this._onFocusChanged();
}
// needed for DND
get metaWindow() {
return this._window;
}
_onDestroy() {
this._window.disconnect(this._sizeChangedId);
this._window.disconnect(this._positionChangedId);
this._window.disconnect(this._minimizedChangedId);
global.display.disconnect(this._focusChangedId);
}
_onFocusChanged() {
if (global.display.focus_window === this._window)
this.add_style_class_name('active');
else
this.remove_style_class_name('active');
}
_updateVisible() {
const monitor = Main.layoutManager.findIndexForActor(this);
const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor);
this.visible = this._window.get_frame_rect().overlap(workArea) &&
this._window.window_type !== Meta.WindowType.DESKTOP &&
this._window.showing_on_its_workspace();
}
});
let WorkspaceLayout = GObject.registerClass(
class WorkspaceLayout extends Clutter.LayoutManager {
vfunc_get_preferred_width() {
return [0, 0];
}
vfunc_get_preferred_height() {
return [0, 0];
}
vfunc_allocate(container, box) {
const monitor = Main.layoutManager.findIndexForActor(container);
const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor);
const hscale = box.get_width() / workArea.width;
const vscale = box.get_height() / workArea.height;
for (const child of container) {
const childBox = new Clutter.ActorBox();
const frameRect = child.metaWindow.get_frame_rect();
childBox.set_size(
Math.round(Math.min(frameRect.width, workArea.width) * hscale),
Math.round(Math.min(frameRect.height, workArea.height) * vscale));
childBox.set_origin(
Math.round((frameRect.x - workArea.x) * hscale),
Math.round((frameRect.y - workArea.y) * vscale));
child.allocate(childBox);
}
}
});
let WorkspaceThumbnail = GObject.registerClass(
class WorkspaceThumbnail extends St.Button {
_init(index) {
super._init({
style_class: 'workspace',
child: new Clutter.Actor({
layout_manager: new WorkspaceLayout(),
clip_to_allocation: true,
}),
});
this._tooltip = new St.Label({
style_class: 'dash-label',
visible: false,
});
Main.uiGroup.add_child(this._tooltip);
this.connect('destroy', this._onDestroy.bind(this));
this.connect('notify::hover', this._syncTooltip.bind(this));
this._index = index;
this._delegate = this; // needed for DND
this._windowPreviews = new Map();
let workspaceManager = global.workspace_manager;
this._workspace = workspaceManager.get_workspace_by_index(index);
this._windowAddedId = this._workspace.connect('window-added',
(ws, window) => {
this._addWindow(window);
});
this._windowRemovedId = this._workspace.connect('window-removed',
(ws, window) => {
this._removeWindow(window);
});
this._restackedId = global.display.connect('restacked',
this._onRestacked.bind(this));
this._workspace.list_windows().forEach(w => this._addWindow(w));
this._onRestacked();
}
acceptDrop(source) {
if (!source.metaWindow)
return false;
this._moveWindow(source.metaWindow);
return true;
}
handleDragOver(source) {
if (source.metaWindow)
return DND.DragMotionResult.MOVE_DROP;
else
return DND.DragMotionResult.CONTINUE;
}
_addWindow(window) {
if (this._windowPreviews.has(window))
return;
let preview = new WindowPreview(window);
preview.connect('clicked', (a, btn) => this.emit('clicked', btn));
this._windowPreviews.set(window, preview);
this.child.add_child(preview);
}
_removeWindow(window) {
let preview = this._windowPreviews.get(window);
if (!preview)
return;
this._windowPreviews.delete(window);
preview.destroy();
}
_onRestacked() {
let lastPreview = null;
let windows = global.get_window_actors().map(a => a.meta_window);
for (let i = 0; i < windows.length; i++) {
let preview = this._windowPreviews.get(windows[i]);
if (!preview)
continue;
this.child.set_child_above_sibling(preview, lastPreview);
lastPreview = preview;
}
}
_moveWindow(window) {
let monitorIndex = Main.layoutManager.findIndexForActor(this);
if (monitorIndex !== window.get_monitor())
window.move_to_monitor(monitorIndex);
window.change_workspace_by_index(this._index, false);
}
on_clicked() {
let ws = global.workspace_manager.get_workspace_by_index(this._index);
if (ws)
ws.activate(global.get_current_time());
}
_syncTooltip() {
if (this.hover) {
this._tooltip.set({
text: Meta.prefs_get_workspace_name(this._index),
visible: true,
opacity: 0,
});
const [stageX, stageY] = this.get_transformed_position();
const thumbWidth = this.allocation.get_width();
const tipWidth = this._tooltip.width;
const tipHeight = this._tooltip.height;
const xOffset = Math.floor((thumbWidth - tipWidth) / 2);
const monitor = Main.layoutManager.findMonitorForActor(this);
const x = Math.clamp(
stageX + xOffset,
monitor.x,
monitor.x + monitor.width - tipWidth);
const y = stageY - tipHeight - TOOLTIP_OFFSET;
this._tooltip.set_position(x, y);
}
this._tooltip.ease({
opacity: this.hover ? 255 : 0,
duration: TOOLTIP_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => (this._tooltip.visible = this.hover),
});
}
_onDestroy() {
this._tooltip.destroy();
this._workspace.disconnect(this._windowAddedId);
this._workspace.disconnect(this._windowRemovedId);
global.display.disconnect(this._restackedId);
}
});
var WorkspaceIndicator = GObject.registerClass(
class WorkspaceIndicator extends PanelMenu.Button {
_init() {
super._init(0.0, _('Workspace Indicator'), true);
this.setMenu(new PopupMenu.PopupMenu(this, 0.0, St.Side.BOTTOM));
this.add_style_class_name('window-list-workspace-indicator');
this.remove_style_class_name('panel-button');
this.menu.actor.remove_style_class_name('panel-menu');
let container = new St.Widget({
layout_manager: new Clutter.BinLayout(),
x_expand: true,
y_expand: true,
});
this.add_actor(container);
let workspaceManager = global.workspace_manager;
this._currentWorkspace = workspaceManager.get_active_workspace_index();
this._statusLabel = new St.Label({ text: this._getStatusText() });
this._statusBin = new St.Bin({
style_class: 'status-label-bin',
x_expand: true,
y_expand: true,
child: this._statusLabel,
});
container.add_actor(this._statusBin);
this._thumbnailsBox = new St.BoxLayout({
style_class: 'workspaces-box',
y_expand: true,
reactive: true,
});
this._thumbnailsBox.connect('scroll-event',
this._onScrollEvent.bind(this));
container.add_actor(this._thumbnailsBox);
this._workspacesItems = [];
this._workspaceManagerSignals = [
workspaceManager.connect('notify::n-workspaces',
this._nWorkspacesChanged.bind(this)),
workspaceManager.connect_after('workspace-switched',
this._onWorkspaceSwitched.bind(this)),
workspaceManager.connect('notify::layout-rows',
this._updateThumbnailVisibility.bind(this)),
];
this.connect('scroll-event', this._onScrollEvent.bind(this));
this._updateMenu();
this._updateThumbnails();
this._updateThumbnailVisibility();
this._settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.wm.preferences' });
this._settingsChangedId = this._settings.connect(
'changed::workspace-names', this._updateMenuLabels.bind(this));
}
_onDestroy() {
for (let i = 0; i < this._workspaceManagerSignals.length; i++)
global.workspace_manager.disconnect(this._workspaceManagerSignals[i]);
if (this._settingsChangedId) {
this._settings.disconnect(this._settingsChangedId);
this._settingsChangedId = 0;
}
super._onDestroy();
}
_updateThumbnailVisibility() {
const { workspaceManager } = global;
const vertical = workspaceManager.layout_rows === -1;
const useMenu =
vertical || workspaceManager.n_workspaces > MAX_THUMBNAILS;
this.reactive = useMenu;
this._statusBin.visible = useMenu;
this._thumbnailsBox.visible = !useMenu;
}
_onWorkspaceSwitched() {
let workspaceManager = global.workspace_manager;
this._currentWorkspace = workspaceManager.get_active_workspace_index();
this._updateMenuOrnament();
this._updateActiveThumbnail();
this._statusLabel.set_text(this._getStatusText());
}
_nWorkspacesChanged() {
this._updateMenu();
this._updateThumbnails();
this._updateThumbnailVisibility();
}
_updateMenuOrnament() {
for (let i = 0; i < this._workspacesItems.length; i++) {
this._workspacesItems[i].setOrnament(i === this._currentWorkspace
? PopupMenu.Ornament.DOT
: PopupMenu.Ornament.NONE);
}
}
_updateActiveThumbnail() {
let thumbs = this._thumbnailsBox.get_children();
for (let i = 0; i < thumbs.length; i++) {
if (i === this._currentWorkspace)
thumbs[i].add_style_class_name('active');
else
thumbs[i].remove_style_class_name('active');
}
}
_getStatusText() {
let workspaceManager = global.workspace_manager;
let current = workspaceManager.get_active_workspace_index();
let total = workspaceManager.n_workspaces;
return '%d / %d'.format(current + 1, total);
}
_updateMenuLabels() {
for (let i = 0; i < this._workspacesItems.length; i++) {
let item = this._workspacesItems[i];
let name = Meta.prefs_get_workspace_name(i);
item.label.text = name;
}
}
_updateMenu() {
let workspaceManager = global.workspace_manager;
this.menu.removeAll();
this._workspacesItems = [];
this._currentWorkspace = workspaceManager.get_active_workspace_index();
for (let i = 0; i < workspaceManager.n_workspaces; i++) {
let name = Meta.prefs_get_workspace_name(i);
let item = new PopupMenu.PopupMenuItem(name);
item.workspaceId = i;
item.connect('activate', () => {
this._activate(item.workspaceId);
});
if (i === this._currentWorkspace)
item.setOrnament(PopupMenu.Ornament.DOT);
this.menu.addMenuItem(item);
this._workspacesItems[i] = item;
}
this._statusLabel.set_text(this._getStatusText());
}
_updateThumbnails() {
let workspaceManager = global.workspace_manager;
this._thumbnailsBox.destroy_all_children();
for (let i = 0; i < workspaceManager.n_workspaces; i++) {
let thumb = new WorkspaceThumbnail(i);
this._thumbnailsBox.add_actor(thumb);
}
this._updateActiveThumbnail();
}
_activate(index) {
let workspaceManager = global.workspace_manager;
if (index >= 0 && index < workspaceManager.n_workspaces) {
let metaWorkspace = workspaceManager.get_workspace_by_index(index);
metaWorkspace.activate(global.get_current_time());
}
}
_onScrollEvent(actor, event) {
let direction = event.get_scroll_direction();
let diff = 0;
if (direction === Clutter.ScrollDirection.DOWN)
diff = 1;
else if (direction === Clutter.ScrollDirection.UP)
diff = -1;
else
return;
let newIndex = this._currentWorkspace + diff;
this._activate(newIndex);
}
});

View File

@@ -0,0 +1,3 @@
EXTENSION_ID = windowsNavigator
include ../../extension.mk

View File

@@ -1,188 +1,154 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
/* exported init */
const { Clutter, Graphene, GObject, St } = imports.gi;
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const St = imports.gi.St;
const Main = imports.ui.main;
const OverviewControls = imports.ui.overviewControls;
const Workspace = imports.ui.workspace;
const WorkspacesView = imports.ui.workspacesView;
const WINDOW_SLOT = 4;
var MyWorkspace = GObject.registerClass(
class MyWorkspace extends Workspace.Workspace {
_init(...args) {
super._init(...args);
if (this.metaWorkspace && this.metaWorkspace.index() < 9) {
this._tip = new St.Label({
style_class: 'extension-windowsNavigator-window-tooltip',
visible: false,
});
this.add_actor(this._tip);
this.connect('notify::scale-x', () => {
this._tip.set_scale(1 / this.scale_x, 1 / this.scale_x);
});
} else {
this._tip = null;
}
function injectToFunction(parent, name, func) {
let origin = parent[name];
parent[name] = function() {
let ret;
ret = origin.apply(this, arguments);
if (ret === undefined)
ret = func.apply(this, arguments);
return ret;
}
return origin;
}
vfunc_allocate(box) {
super.vfunc_allocate(box);
let winInjections, workspaceInjections, workViewInjections, createdActors, connectedSignals;
if (this._tip)
this._tip.allocate_preferred_size(0, 0);
function resetState() {
winInjections = { };
workspaceInjections = { };
workViewInjections = { };
createdActors = [ ];
connectedSignals = [ ];
}
function enable() {
resetState();
Workspace.WindowOverlay.prototype.showTooltip = function() {
this._text.raise_top();
this._text.show();
this._text.text = (this._windowClone.slotId + 1).toString();
}
winInjections['showTooltip'] = undefined;
showTooltip() {
if (!this._tip)
Workspace.WindowOverlay.prototype.hideTooltip = function() {
if (this._text && this._text.visible)
this._text.hide();
}
winInjections['hideTooltip'] = undefined;
Workspace.Workspace.prototype.showTooltip = function() {
if (this._tip == null || this._actualGeometry == null)
return;
this._tip.text = (this.metaWorkspace.index() + 1).toString();
// Hand code this instead of using _getSpacingAndPadding
// because that fails on empty workspaces
let node = this.actor.get_theme_node();
let padding = {
left: node.get_padding(St.Side.LEFT),
top: node.get_padding(St.Side.TOP),
bottom: node.get_padding(St.Side.BOTTOM),
right: node.get_padding(St.Side.RIGHT),
};
let area = Workspace.padArea(this._actualGeometry, padding);
this._tip.x = area.x;
this._tip.y = area.y;
this._tip.show();
this.set_child_below_sibling(this._tip, null);
this._tip.raise_top();
}
workspaceInjections['showTooltip'] = undefined;
hideTooltip() {
if (this._tip)
this._tip.hide();
Workspace.Workspace.prototype.hideTooltip = function() {
if (this._tip == null)
return;
if (!this._tip.get_parent())
return;
this._tip.hide();
}
workspaceInjections['hideTooltip'] = undefined;
getWindowWithTooltip(id) {
const { layoutManager } = this._container;
const slot = layoutManager._windowSlots[id - 1];
return slot ? slot[WINDOW_SLOT].metaWindow : null;
Workspace.Workspace.prototype.getWindowWithTooltip = function(id) {
for (let i = 0; i < this._windows.length; i++) {
if ((this._windows[i].slotId + 1) == id)
return this._windows[i].metaWindow;
}
return null;
}
workspaceInjections['getWindowWithTooltip'] = undefined;
showWindowsTooltips() {
const { layoutManager } = this._container;
for (let i = 0; i < layoutManager._windowSlots.length; i++) {
if (layoutManager._windowSlots[i])
layoutManager._windowSlots[i][WINDOW_SLOT].showTooltip(`${i + 1}`);
Workspace.Workspace.prototype.showWindowsTooltips = function() {
for (let i in this._windowOverlays) {
if (this._windowOverlays[i] != null)
this._windowOverlays[i].showTooltip();
}
}
workspaceInjections['showWindowsTooltips'] = undefined;
hideWindowsTooltips() {
const { layoutManager } = this._container;
for (let i in layoutManager._windowSlots) {
if (layoutManager._windowSlots[i])
layoutManager._windowSlots[i][WINDOW_SLOT].hideTooltip();
Workspace.Workspace.prototype.hideWindowsTooltips = function() {
for (let i in this._windowOverlays) {
if (this._windowOverlays[i] != null)
this._windowOverlays[i].hideTooltip();
}
}
workspaceInjections['hideWindowsTooltips'] = undefined;
// overriding _addWindowClone to apply the tooltip patch on the cloned
// windowPreview
_addWindowClone(metaWindow) {
const clone = super._addWindowClone(metaWindow);
// appling the tooltip patch
(function patchPreview() {
this._text = new St.Label({
style_class: 'extension-windowsNavigator-window-tooltip',
visible: false,
});
this._text.add_constraint(new Clutter.BindConstraint({
source: this.windowContainer,
coordinate: Clutter.BindCoordinate.POSITION,
}));
this._text.add_constraint(new Clutter.AlignConstraint({
source: this.windowContainer,
align_axis: Clutter.AlignAxis.X_AXIS,
pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
factor: this._closeButtonSide === St.Side.LEFT ? 1 : 0,
}));
this._text.add_constraint(new Clutter.AlignConstraint({
source: this.windowContainer,
align_axis: Clutter.AlignAxis.Y_AXIS,
pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
factor: 0,
}));
this.add_child(this._text);
}).call(clone);
clone.showTooltip = function (text) {
this._text.set({ text });
this._text.show();
};
clone.hideTooltip = function () {
if (this._text && this._text.visible)
this._text.hide();
};
return clone;
}
});
var MyWorkspacesView = GObject.registerClass(
class MyWorkspacesView extends WorkspacesView.WorkspacesView {
_init(...args) {
super._init(...args);
this._pickWorkspace = false;
this._pickWindow = false;
this._keyPressEventId =
global.stage.connect('key-press-event', this._onKeyPress.bind(this));
this._keyReleaseEventId =
global.stage.connect('key-release-event', this._onKeyRelease.bind(this));
}
_onDestroy() {
super._onDestroy();
global.stage.disconnect(this._keyPressEventId);
global.stage.disconnect(this._keyReleaseEventId);
}
_hideTooltips() {
if (global.stage.get_key_focus() === global.stage)
WorkspacesView.WorkspacesView.prototype._hideTooltips = function() {
if (global.stage.get_key_focus() == global.stage)
global.stage.set_key_focus(this._prevFocusActor);
this._pickWindow = false;
for (let i = 0; i < this._workspaces.length; i++)
this._workspaces[i].hideWindowsTooltips();
}
workViewInjections['_hideTooltips'] = undefined;
_hideWorkspacesTooltips() {
WorkspacesView.WorkspacesView.prototype._hideWorkspacesTooltips = function() {
global.stage.set_key_focus(this._prevFocusActor);
this._pickWorkspace = false;
for (let i = 0; i < this._workspaces.length; i++)
this._workspaces[i].hideTooltip();
}
workViewInjections['_hideWorkspacesTooltips'] = undefined;
_onKeyRelease(s, o) {
WorkspacesView.WorkspacesView.prototype._onKeyRelease = function(s, o) {
if (this._pickWindow &&
(o.get_key_symbol() === Clutter.KEY_Alt_L ||
o.get_key_symbol() === Clutter.KEY_Alt_R))
(o.get_key_symbol() == Clutter.KEY_Alt_L ||
o.get_key_symbol() == Clutter.KEY_Alt_R))
this._hideTooltips();
if (this._pickWorkspace &&
(o.get_key_symbol() === Clutter.KEY_Control_L ||
o.get_key_symbol() === Clutter.KEY_Control_R))
(o.get_key_symbol() == Clutter.KEY_Control_L ||
o.get_key_symbol() == Clutter.KEY_Control_R))
this._hideWorkspacesTooltips();
}
workViewInjections['_onKeyRelease'] = undefined;
_onKeyPress(s, o) {
const { ControlsState } = OverviewControls;
if (this._overviewAdjustment.value !== ControlsState.WINDOW_PICKER)
WorkspacesView.WorkspacesView.prototype._onKeyPress = function(s, o) {
if(Main.overview.viewSelector._activePage != Main.overview.viewSelector._workspacesPage)
return false;
let workspaceManager = global.workspace_manager;
if ((o.get_key_symbol() === Clutter.KEY_Alt_L ||
o.get_key_symbol() === Clutter.KEY_Alt_R) &&
!this._pickWorkspace) {
if ((o.get_key_symbol() == Clutter.KEY_Alt_L ||
o.get_key_symbol() == Clutter.KEY_Alt_R)
&& !this._pickWorkspace) {
this._prevFocusActor = global.stage.get_key_focus();
global.stage.set_key_focus(null);
this._active = workspaceManager.get_active_workspace_index();
this._active = global.screen.get_active_workspace_index();
this._pickWindow = true;
this._workspaces[workspaceManager.get_active_workspace_index()].showWindowsTooltips();
this._workspaces[global.screen.get_active_workspace_index()].showWindowsTooltips();
return true;
}
if ((o.get_key_symbol() === Clutter.KEY_Control_L ||
o.get_key_symbol() === Clutter.KEY_Control_R) &&
!this._pickWindow) {
if ((o.get_key_symbol() == Clutter.KEY_Control_L ||
o.get_key_symbol() == Clutter.KEY_Control_R)
&& !this._pickWindow) {
this._prevFocusActor = global.stage.get_key_focus();
global.stage.set_key_focus(null);
this._pickWorkspace = true;
@@ -191,17 +157,17 @@ class MyWorkspacesView extends WorkspacesView.WorkspacesView {
return true;
}
if (global.stage.get_key_focus() !== global.stage)
if (global.stage.get_key_focus() != global.stage)
return false;
// ignore shift presses, they're required to get numerals in azerty keyboards
if ((this._pickWindow || this._pickWorkspace) &&
(o.get_key_symbol() === Clutter.KEY_Shift_L ||
o.get_key_symbol() === Clutter.KEY_Shift_R))
(o.get_key_symbol() == Clutter.KEY_Shift_L ||
o.get_key_symbol() == Clutter.KEY_Shift_R))
return true;
if (this._pickWindow) {
if (this._active !== workspaceManager.get_active_workspace_index()) {
if (this._active != global.screen.get_active_workspace_index()) {
this._hideTooltips();
return false;
}
@@ -243,28 +209,80 @@ class MyWorkspacesView extends WorkspacesView.WorkspacesView {
}
return false;
}
});
workViewInjections['_onKeyPress'] = undefined;
class Extension {
constructor() {
this._origWorkspace = Workspace.Workspace;
this._origWorkspacesView = WorkspacesView.WorkspacesView;
}
winInjections['_init'] = injectToFunction(Workspace.WindowOverlay.prototype, '_init', function(windowClone, parentActor) {
this._id = null;
createdActors.push(this._text = new St.Label({ style_class: 'extension-windowsNavigator-window-tooltip' }));
this._text.hide();
parentActor.add_actor(this._text);
});
enable() {
Workspace.Workspace = MyWorkspace;
WorkspacesView.WorkspacesView = MyWorkspacesView;
}
winInjections['relayout'] = injectToFunction(Workspace.WindowOverlay.prototype, 'relayout', function(animate) {
let [cloneX, cloneY, cloneWidth, cloneHeight] = this._windowClone.slot;
disable() {
Workspace.Workspace = this._origWorkspace;
WorkspacesView.WorkspacesView = this._origWorkspacesView;
}
let textX = cloneX - 2;
let textY = cloneY - 2;
this._text.set_position(Math.floor(textX) + 5, Math.floor(textY) + 5);
this._text.raise_top();
});
workspaceInjections['_init'] = injectToFunction(Workspace.Workspace.prototype, '_init', function(metaWorkspace) {
if (metaWorkspace && metaWorkspace.index() < 9) {
createdActors.push(this._tip = new St.Label({ style_class: 'extension-windowsNavigator-window-tooltip',
visible: false }));
this.actor.add_actor(this._tip);
let signalId = this.actor.connect('notify::scale-x', Lang.bind(this, function() {
this._tip.set_scale(1 / this.actor.scale_x, 1 / this.actor.scale_x);
}));
connectedSignals.push({ obj: this.actor, id: signalId });
} else
this._tip = null;
});
workViewInjections['_init'] = injectToFunction(WorkspacesView.WorkspacesView.prototype, '_init', function(width, height, x, y, workspaces) {
this._pickWorkspace = false;
this._pickWindow = false;
this._keyPressEventId = global.stage.connect('key-press-event', Lang.bind(this, this._onKeyPress));
this._keyReleaseEventId = global.stage.connect('key-release-event', Lang.bind(this, this._onKeyRelease));
connectedSignals.push({ obj: global.stage, id: this._keyPressEventId });
connectedSignals.push({ obj: global.stage, id: this._keyReleaseEventId });
});
workViewInjections['_onDestroy'] = injectToFunction(WorkspacesView.WorkspacesView.prototype, '_onDestroy', function() {
global.stage.disconnect(this._keyPressEventId);
global.stage.disconnect(this._keyReleaseEventId);
connectedSignals = [ ];
});
}
function removeInjection(object, injection, name) {
if (injection[name] === undefined)
delete object[name];
else
object[name] = injection[name];
}
function disable() {
let i;
for (i in workspaceInjections)
removeInjection(Workspace.Workspace.prototype, workspaceInjections, i);
for (i in winInjections)
removeInjection(Workspace.WindowOverlay.prototype, winInjections, i);
for (i in workViewInjections)
removeInjection(WorkspacesView.WorkspacesView.prototype, workViewInjections, i);
for each (i in connectedSignals)
i.obj.disconnect(i.id);
for each (i in createdActors)
i.destroy();
resetState();
}
/**
* @returns {Extension} - the extension's state object
*/
function init() {
return new Extension();
/* do nothing */
}

View File

@@ -1,5 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)

View File

@@ -0,0 +1,5 @@
EXTENSION_ID = workspace-indicator
EXTRA_MODULES = prefs.js
include ../../extension.mk

View File

@@ -1,461 +1,144 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
/* exported init enable disable */
const { Clutter, Gio, GObject, Meta, St } = imports.gi;
const DND = imports.ui.dnd;
const ExtensionUtils = imports.misc.extensionUtils;
const Main = imports.ui.main;
const Gio = imports.gi.Gio;
const Meta = imports.gi.Meta;
const Clutter = imports.gi.Clutter;
const St = imports.gi.St;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Panel = imports.ui.panel;
const _ = ExtensionUtils.gettext;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const Main = imports.ui.main;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
const WORKSPACE_SCHEMA = 'org.gnome.desktop.wm.preferences';
const WORKSPACE_KEY = 'workspace-names';
const TOOLTIP_OFFSET = 6;
const TOOLTIP_ANIMATION_TIME = 150;
const WorkspaceIndicator = new Lang.Class({
Name: 'WorkspaceIndicator.WorkspaceIndicator',
Extends: PanelMenu.Button,
const MAX_THUMBNAILS = 6;
_init: function(){
this.parent(0.0, _("Workspace Indicator"));
let WindowPreview = GObject.registerClass(
class WindowPreview extends St.Button {
_init(window) {
super._init({
style_class: 'workspace-indicator-window-preview',
});
this._currentWorkspace = global.screen.get_active_workspace().index();
this.statusLabel = new St.Label({ y_align: Clutter.ActorAlign.CENTER,
text: this._labelText() });
this._delegate = this;
DND.makeDraggable(this, { restoreOnSuccess: true });
this.actor.add_actor(this.statusLabel);
this._window = window;
this.workspacesItems = [];
this._workspaceSection = new PopupMenu.PopupMenuSection();
this.menu.addMenuItem(this._workspaceSection);
this.connect('destroy', this._onDestroy.bind(this));
this._screenSignals = [];
this._screenSignals.push(global.screen.connect_after('workspace-added', Lang.bind(this,this._createWorkspacesSection)));
this._screenSignals.push(global.screen.connect_after('workspace-removed', Lang.bind(this,this._createWorkspacesSection)));
this._screenSignals.push(global.screen.connect_after('workspace-switched', Lang.bind(this,this._updateIndicator)));
this._sizeChangedId = this._window.connect('size-changed',
() => this.queue_relayout());
this._positionChangedId = this._window.connect('position-changed',
() => {
this._updateVisible();
this.queue_relayout();
});
this._minimizedChangedId = this._window.connect('notify::minimized',
this._updateVisible.bind(this));
this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
this._createWorkspacesSection();
this._focusChangedId = global.display.connect('notify::focus-window',
this._onFocusChanged.bind(this));
this._onFocusChanged();
}
// needed for DND
get metaWindow() {
return this._window;
}
_onDestroy() {
this._window.disconnect(this._sizeChangedId);
this._window.disconnect(this._positionChangedId);
this._window.disconnect(this._minimizedChangedId);
global.display.disconnect(this._focusChangedId);
}
_onFocusChanged() {
if (global.display.focus_window === this._window)
this.add_style_class_name('active');
else
this.remove_style_class_name('active');
}
_updateVisible() {
const monitor = Main.layoutManager.findIndexForActor(this);
const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor);
this.visible = this._window.get_frame_rect().overlap(workArea) &&
this._window.window_type !== Meta.WindowType.DESKTOP &&
this._window.showing_on_its_workspace();
}
});
let WorkspaceLayout = GObject.registerClass(
class WorkspaceLayout extends Clutter.LayoutManager {
vfunc_get_preferred_width() {
return [0, 0];
}
vfunc_get_preferred_height() {
return [0, 0];
}
vfunc_allocate(container, box) {
const monitor = Main.layoutManager.findIndexForActor(container);
const workArea = Main.layoutManager.getWorkAreaForMonitor(monitor);
const hscale = box.get_width() / workArea.width;
const vscale = box.get_height() / workArea.height;
for (const child of container) {
const childBox = new Clutter.ActorBox();
const frameRect = child.metaWindow.get_frame_rect();
childBox.set_size(
Math.round(Math.min(frameRect.width, workArea.width) * hscale),
Math.round(Math.min(frameRect.height, workArea.height) * vscale));
childBox.set_origin(
Math.round((frameRect.x - workArea.x) * hscale),
Math.round((frameRect.y - workArea.y) * vscale));
child.allocate(childBox);
}
}
});
let WorkspaceThumbnail = GObject.registerClass(
class WorkspaceThumbnail extends St.Button {
_init(index) {
super._init({
style_class: 'workspace',
child: new Clutter.Actor({
layout_manager: new WorkspaceLayout(),
clip_to_allocation: true,
}),
});
this._tooltip = new St.Label({
style_class: 'dash-label',
visible: false,
});
Main.uiGroup.add_child(this._tooltip);
this.connect('destroy', this._onDestroy.bind(this));
this.connect('notify::hover', this._syncTooltip.bind(this));
this._index = index;
this._delegate = this; // needed for DND
this._windowPreviews = new Map();
let workspaceManager = global.workspace_manager;
this._workspace = workspaceManager.get_workspace_by_index(index);
this._windowAddedId = this._workspace.connect('window-added',
(ws, window) => {
this._addWindow(window);
});
this._windowRemovedId = this._workspace.connect('window-removed',
(ws, window) => {
this._removeWindow(window);
});
this._restackedId = global.display.connect('restacked',
this._onRestacked.bind(this));
this._workspace.list_windows().forEach(w => this._addWindow(w));
this._onRestacked();
}
acceptDrop(source) {
if (!source.metaWindow)
return false;
this._moveWindow(source.metaWindow);
return true;
}
handleDragOver(source) {
if (source.metaWindow)
return DND.DragMotionResult.MOVE_DROP;
else
return DND.DragMotionResult.CONTINUE;
}
_addWindow(window) {
if (this._windowPreviews.has(window))
return;
let preview = new WindowPreview(window);
preview.connect('clicked', (a, btn) => this.emit('clicked', btn));
this._windowPreviews.set(window, preview);
this.child.add_child(preview);
}
_removeWindow(window) {
let preview = this._windowPreviews.get(window);
if (!preview)
return;
this._windowPreviews.delete(window);
preview.destroy();
}
_onRestacked() {
let lastPreview = null;
let windows = global.get_window_actors().map(a => a.meta_window);
for (let i = 0; i < windows.length; i++) {
let preview = this._windowPreviews.get(windows[i]);
if (!preview)
continue;
this.child.set_child_above_sibling(preview, lastPreview);
lastPreview = preview;
}
}
_moveWindow(window) {
let monitorIndex = Main.layoutManager.findIndexForActor(this);
if (monitorIndex !== window.get_monitor())
window.move_to_monitor(monitorIndex);
window.change_workspace_by_index(this._index, false);
}
on_clicked() {
let ws = global.workspace_manager.get_workspace_by_index(this._index);
if (ws)
ws.activate(global.get_current_time());
}
_syncTooltip() {
if (this.hover) {
this._tooltip.set({
text: Meta.prefs_get_workspace_name(this._index),
visible: true,
opacity: 0,
});
const [stageX, stageY] = this.get_transformed_position();
const thumbWidth = this.allocation.get_width();
const thumbHeight = this.allocation.get_height();
const tipWidth = this._tooltip.width;
const xOffset = Math.floor((thumbWidth - tipWidth) / 2);
const monitor = Main.layoutManager.findMonitorForActor(this);
const x = Math.clamp(
stageX + xOffset,
monitor.x,
monitor.x + monitor.width - tipWidth);
const y = stageY + thumbHeight + TOOLTIP_OFFSET;
this._tooltip.set_position(x, y);
}
this._tooltip.ease({
opacity: this.hover ? 255 : 0,
duration: TOOLTIP_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => (this._tooltip.visible = this.hover),
});
}
_onDestroy() {
this._tooltip.destroy();
this._workspace.disconnect(this._windowAddedId);
this._workspace.disconnect(this._windowRemovedId);
global.display.disconnect(this._restackedId);
}
});
let WorkspaceIndicator = GObject.registerClass(
class WorkspaceIndicator extends PanelMenu.Button {
_init() {
super._init(0.0, _('Workspace Indicator'));
let container = new St.Widget({
layout_manager: new Clutter.BinLayout(),
x_expand: true,
y_expand: true,
});
this.add_actor(container);
let workspaceManager = global.workspace_manager;
this._currentWorkspace = workspaceManager.get_active_workspace_index();
this._statusLabel = new St.Label({
style_class: 'panel-workspace-indicator',
y_align: Clutter.ActorAlign.CENTER,
text: this._labelText(),
});
container.add_actor(this._statusLabel);
this._thumbnailsBox = new St.BoxLayout({
style_class: 'panel-workspace-indicator-box',
y_expand: true,
reactive: true,
});
container.add_actor(this._thumbnailsBox);
this._workspacesItems = [];
this._workspaceSection = new PopupMenu.PopupMenuSection();
this.menu.addMenuItem(this._workspaceSection);
this._workspaceManagerSignals = [
workspaceManager.connect_after('notify::n-workspaces',
this._nWorkspacesChanged.bind(this)),
workspaceManager.connect_after('workspace-switched',
this._onWorkspaceSwitched.bind(this)),
workspaceManager.connect('notify::layout-rows',
this._updateThumbnailVisibility.bind(this)),
];
this.connect('scroll-event', this._onScrollEvent.bind(this));
this._thumbnailsBox.connect('scroll-event', this._onScrollEvent.bind(this));
this._createWorkspacesSection();
this._updateThumbnails();
this._updateThumbnailVisibility();
//styling
this.statusLabel.add_style_class_name('panel-workspace-indicator');
this._settings = new Gio.Settings({ schema_id: WORKSPACE_SCHEMA });
this._settingsChangedId = this._settings.connect(
`changed::${WORKSPACE_KEY}`,
this._updateMenuLabels.bind(this));
}
this._settingsChangedId = this._settings.connect('changed::' + WORKSPACE_KEY, Lang.bind(this, this._createWorkspacesSection));
},
_onDestroy() {
for (let i = 0; i < this._workspaceManagerSignals.length; i++)
global.workspace_manager.disconnect(this._workspaceManagerSignals[i]);
destroy: function() {
for (let i = 0; i < this._screenSignals.length; i++)
global.screen.disconnect(this._screenSignals[i]);
if (this._settingsChangedId) {
this._settings.disconnect(this._settingsChangedId);
this._settingsChangedId = 0;
}
Main.panel.set_offscreen_redirect(Clutter.OffscreenRedirect.ALWAYS);
this.parent();
},
super._onDestroy();
}
_updateIndicator: function() {
this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.NONE);
this._currentWorkspace = global.screen.get_active_workspace().index();
this.workspacesItems[this._currentWorkspace].setOrnament(PopupMenu.Ornament.DOT);
_updateThumbnailVisibility() {
const { workspaceManager } = global;
const vertical = workspaceManager.layout_rows === -1;
const useMenu =
vertical || workspaceManager.n_workspaces > MAX_THUMBNAILS;
this.reactive = useMenu;
this.statusLabel.set_text(this._labelText());
},
this._statusLabel.visible = useMenu;
this._thumbnailsBox.visible = !useMenu;
_labelText : function(workspaceIndex) {
if(workspaceIndex == undefined) {
workspaceIndex = this._currentWorkspace;
return (workspaceIndex + 1).toString();
}
return Meta.prefs_get_workspace_name(workspaceIndex);
},
// Disable offscreen-redirect when showing the workspace switcher
// so that clip-to-allocation works
Main.panel.set_offscreen_redirect(useMenu
? Clutter.OffscreenRedirect.ALWAYS
: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY);
}
_createWorkspacesSection : function() {
this._workspaceSection.removeAll();
this.workspacesItems = [];
this._currentWorkspace = global.screen.get_active_workspace().index();
_onWorkspaceSwitched() {
this._currentWorkspace = global.workspace_manager.get_active_workspace_index();
let i = 0;
for(; i < global.screen.n_workspaces; i++) {
this.workspacesItems[i] = new PopupMenu.PopupMenuItem(this._labelText(i));
this._workspaceSection.addMenuItem(this.workspacesItems[i]);
this.workspacesItems[i].workspaceId = i;
this.workspacesItems[i].label_actor = this.statusLabel;
let self = this;
this.workspacesItems[i].connect('activate', Lang.bind(this, function(actor, event) {
this._activate(actor.workspaceId);
}));
this._updateMenuOrnament();
this._updateActiveThumbnail();
if (i == this._currentWorkspace)
this.workspacesItems[i].setOrnament(PopupMenu.Ornament.DOT);
}
this._statusLabel.set_text(this._labelText());
}
this.statusLabel.set_text(this._labelText());
},
_nWorkspacesChanged() {
this._createWorkspacesSection();
this._updateThumbnails();
this._updateThumbnailVisibility();
}
_activate : function (index) {
if(index >= 0 && index < global.screen.n_workspaces) {
let metaWorkspace = global.screen.get_workspace_by_index(index);
metaWorkspace.activate(global.get_current_time());
}
},
_updateMenuOrnament() {
for (let i = 0; i < this._workspacesItems.length; i++) {
this._workspacesItems[i].setOrnament(i === this._currentWorkspace
? PopupMenu.Ornament.DOT
: PopupMenu.Ornament.NONE);
}
}
_onScrollEvent : function(actor, event) {
let direction = event.get_scroll_direction();
let diff = 0;
if (direction == Clutter.ScrollDirection.DOWN) {
diff = 1;
} else if (direction == Clutter.ScrollDirection.UP) {
diff = -1;
} else {
return;
}
_updateActiveThumbnail() {
let thumbs = this._thumbnailsBox.get_children();
for (let i = 0; i < thumbs.length; i++) {
if (i === this._currentWorkspace)
thumbs[i].add_style_class_name('active');
else
thumbs[i].remove_style_class_name('active');
}
}
_labelText(workspaceIndex) {
if (workspaceIndex === undefined) {
workspaceIndex = this._currentWorkspace;
return (workspaceIndex + 1).toString();
}
return Meta.prefs_get_workspace_name(workspaceIndex);
}
_updateMenuLabels() {
for (let i = 0; i < this._workspacesItems.length; i++)
this._workspacesItems[i].label.text = this._labelText(i);
}
_createWorkspacesSection() {
let workspaceManager = global.workspace_manager;
this._workspaceSection.removeAll();
this._workspacesItems = [];
this._currentWorkspace = workspaceManager.get_active_workspace_index();
let i = 0;
for (; i < workspaceManager.n_workspaces; i++) {
this._workspacesItems[i] = new PopupMenu.PopupMenuItem(this._labelText(i));
this._workspaceSection.addMenuItem(this._workspacesItems[i]);
this._workspacesItems[i].workspaceId = i;
this._workspacesItems[i].label_actor = this._statusLabel;
this._workspacesItems[i].connect('activate', (actor, _event) => {
this._activate(actor.workspaceId);
});
if (i === this._currentWorkspace)
this._workspacesItems[i].setOrnament(PopupMenu.Ornament.DOT);
}
this._statusLabel.set_text(this._labelText());
}
_updateThumbnails() {
let workspaceManager = global.workspace_manager;
this._thumbnailsBox.destroy_all_children();
for (let i = 0; i < workspaceManager.n_workspaces; i++) {
let thumb = new WorkspaceThumbnail(i);
this._thumbnailsBox.add_actor(thumb);
}
this._updateActiveThumbnail();
}
_activate(index) {
let workspaceManager = global.workspace_manager;
if (index >= 0 && index < workspaceManager.n_workspaces) {
let metaWorkspace = workspaceManager.get_workspace_by_index(index);
metaWorkspace.activate(global.get_current_time());
}
}
_onScrollEvent(actor, event) {
let direction = event.get_scroll_direction();
let diff = 0;
if (direction === Clutter.ScrollDirection.DOWN)
diff = 1;
else if (direction === Clutter.ScrollDirection.UP)
diff = -1;
else
return;
let newIndex = global.workspace_manager.get_active_workspace_index() + diff;
this._activate(newIndex);
}
let newIndex = global.screen.get_active_workspace().index() + diff;
this._activate(newIndex);
},
});
/** */
function init() {
ExtensionUtils.initTranslations();
function init(meta) {
Convenience.initTranslations();
}
let _indicator;
/** */
function enable() {
_indicator = new WorkspaceIndicator();
_indicator = new WorkspaceIndicator;
Main.panel.addToStatusArea('workspace-indicator', _indicator);
}
/** */
function disable() {
_indicator.destroy();
}

View File

@@ -1,7 +0,0 @@
extension_data += configure_file(
input: metadata_name + '.in',
output: metadata_name,
configuration: metadata_conf
)
extension_sources += files('prefs.js')

View File

@@ -1,198 +1,219 @@
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
/* exported init buildPrefsWidget */
const { Adw, Gio, GLib, GObject, Gtk, Pango } = imports.gi;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Gettext = imports.gettext.domain('gnome-shell-extensions');
const _ = Gettext.gettext;
const N_ = function(e) { return e };
const ExtensionUtils = imports.misc.extensionUtils;
const _ = ExtensionUtils.gettext;
const N_ = e => e;
const Me = ExtensionUtils.getCurrentExtension();
const Convenience = Me.imports.convenience;
const WORKSPACE_SCHEMA = 'org.gnome.desktop.wm.preferences';
const WORKSPACE_KEY = 'workspace-names';
const WorkspaceSettingsWidget = GObject.registerClass(
class WorkspaceSettingsWidget extends Adw.PreferencesGroup {
_init() {
super._init({
title: _('Workspace Names'),
});
const WorkspaceNameModel = new GObject.Class({
Name: 'WorkspaceIndicator.WorkspaceNameModel',
GTypeName: 'WorkspaceNameModel',
Extends: Gtk.ListStore,
this._list = new Gtk.ListBox({
selection_mode: Gtk.SelectionMode.NONE,
css_classes: ['boxed-list'],
});
this._list.connect('row-activated', (l, row) => row.edit());
this.add(this._list);
Columns: {
LABEL: 0,
},
this._list.append(new NewWorkspaceRow());
_init: function(params) {
this.parent(params);
this.set_column_types([GObject.TYPE_STRING]);
this._actionGroup = new Gio.SimpleActionGroup();
this._list.insert_action_group('workspaces', this._actionGroup);
this._settings = new Gio.Settings({ schema_id: WORKSPACE_SCHEMA });
//this._settings.connect('changed::workspace-names', Lang.bind(this, this._reloadFromSettings));
let action;
action = new Gio.SimpleAction({ name: 'add' });
action.connect('activate', () => {
const names = this._settings.get_strv(WORKSPACE_KEY);
this._settings.set_strv(WORKSPACE_KEY, [
...names,
_('Workspace %d').format(names.length + 1),
]);
});
this._actionGroup.add_action(action);
this._reloadFromSettings();
action = new Gio.SimpleAction({
name: 'remove',
parameter_type: new GLib.VariantType('s'),
});
action.connect('activate', (a, param) => {
const removed = param.deepUnpack();
this._settings.set_strv(WORKSPACE_KEY,
this._settings.get_strv(WORKSPACE_KEY)
.filter(name => name !== removed));
});
this._actionGroup.add_action(action);
// overriding class closure doesn't work, because GtkTreeModel
// plays tricks with marshallers and class closures
this.connect('row-changed', Lang.bind(this, this._onRowChanged));
this.connect('row-inserted', Lang.bind(this, this._onRowInserted));
this.connect('row-deleted', Lang.bind(this, this._onRowDeleted));
},
action = new Gio.SimpleAction({ name: 'update' });
action.connect('activate', () => {
const names = this._getWorkspaceRows().map(row => row.name);
this._settings.set_strv(WORKSPACE_KEY, names);
});
this._actionGroup.add_action(action);
_reloadFromSettings: function() {
if (this._preventChanges)
return;
this._preventChanges = true;
this._settings = new Gio.Settings({
schema_id: WORKSPACE_SCHEMA,
});
this._settings.connect(`changed::${WORKSPACE_KEY}`,
this._sync.bind(this));
this._sync();
}
let newNames = this._settings.get_strv(WORKSPACE_KEY);
_getWorkspaceRows() {
return [...this._list].filter(row => row.name);
}
let i = 0;
let [ok, iter] = this.get_iter_first();
while (ok && i < newNames.length) {
this.set(iter, [this.Columns.LABEL], [newNames[i]]);
_sync() {
const rows = this._getWorkspaceRows();
ok = this.iter_next(iter);
i++;
}
const oldNames = rows.map(row => row.name);
const newNames = this._settings.get_strv(WORKSPACE_KEY);
while (ok)
ok = this.remove(iter);
const removed = oldNames.filter(n => !newNames.includes(n));
const added = newNames.filter(n => !oldNames.includes(n));
for ( ; i < newNames.length; i++) {
iter = this.append();
this.set(iter, [this.Columns.LABEL], [newNames[i]]);
}
removed.forEach(n => this._list.remove(rows.find(r => r.name === n)));
added.forEach(n => {
this._list.insert(new WorkspaceRow(n), newNames.indexOf(n));
});
this._preventChanges = false;
},
_onRowChanged: function(self, path, iter) {
if (this._preventChanges)
return;
this._preventChanges = true;
let index = path.get_indices()[0];
let names = this._settings.get_strv(WORKSPACE_KEY);
if (index >= names.length) {
// fill with blanks
for (let i = names.length; i <= index; i++)
names[i] = '';
}
names[index] = this.get_value(iter, this.Columns.LABEL);
this._settings.set_strv(WORKSPACE_KEY, names);
this._preventChanges = false;
},
_onRowInserted: function(self, path, iter) {
if (this._preventChanges)
return;
this._preventChanges = true;
let index = path.get_indices()[0];
let names = this._settings.get_strv(WORKSPACE_KEY);
let label = this.get_value(iter, this.Columns.LABEL) || '';
names.splice(index, 0, label);
this._settings.set_strv(WORKSPACE_KEY, names);
this._preventChanges = false;
},
_onRowDeleted: function(self, path) {
if (this._preventChanges)
return;
this._preventChanges = true;
let index = path.get_indices()[0];
let names = this._settings.get_strv(WORKSPACE_KEY);
if (index >= names.length)
return;
names.splice(index, 1);
// compact the array
for (let i = names.length -1; i >= 0 && !names[i]; i++)
names.pop();
this._settings.set_strv(WORKSPACE_KEY, names);
this._preventChanges = false;
},
});
const WorkspaceSettingsWidget = new GObject.Class({
Name: 'WorkspaceIndicator.WorkspaceSettingsWidget',
GTypeName: 'WorkspaceSettingsWidget',
Extends: Gtk.Grid,
_init: function(params) {
this.parent(params);
this.margin = 12;
this.orientation = Gtk.Orientation.VERTICAL;
this.add(new Gtk.Label({ label: '<b>' + _("Workspace Names") + '</b>',
use_markup: true, margin_bottom: 6,
hexpand: true, halign: Gtk.Align.START }));
let scrolled = new Gtk.ScrolledWindow({ shadow_type: Gtk.ShadowType.IN });
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
this.add(scrolled);
this._store = new WorkspaceNameModel();
this._treeView = new Gtk.TreeView({ model: this._store,
headers_visible: false,
reorderable: true,
hexpand: true,
vexpand: true
});
let column = new Gtk.TreeViewColumn({ title: _("Name") });
let renderer = new Gtk.CellRendererText({ editable: true });
renderer.connect('edited', Lang.bind(this, this._cellEdited));
column.pack_start(renderer, true);
column.add_attribute(renderer, 'text', this._store.Columns.LABEL);
this._treeView.append_column(column);
scrolled.add(this._treeView);
let toolbar = new Gtk.Toolbar({ icon_size: Gtk.IconSize.SMALL_TOOLBAR });
toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);
let newButton = new Gtk.ToolButton({ icon_name: 'list-add-symbolic' });
newButton.connect('clicked', Lang.bind(this, this._newClicked));
toolbar.add(newButton);
let delButton = new Gtk.ToolButton({ icon_name: 'list-remove-symbolic' });
delButton.connect('clicked', Lang.bind(this, this._delClicked));
toolbar.add(delButton);
let selection = this._treeView.get_selection();
selection.connect('changed',
function() {
delButton.sensitive = selection.count_selected_rows() > 0;
});
delButton.sensitive = selection.count_selected_rows() > 0;
this.add(toolbar);
},
_cellEdited: function(renderer, path, new_text) {
let [ok, iter] = this._store.get_iter_from_string(path);
if (ok)
this._store.set(iter, [this._store.Columns.LABEL], [new_text]);
},
_newClicked: function() {
let iter = this._store.append();
let index = this._store.get_path(iter).get_indices()[0];
let label = _("Workspace %d").format(index + 1);
this._store.set(iter, [this._store.Columns.LABEL], [label]);
},
_delClicked: function() {
let [any, model, iter] = this._treeView.get_selection().get_selected();
if (any)
this._store.remove(iter);
}
});
const WorkspaceRow = GObject.registerClass(
class WorkspaceRow extends Adw.PreferencesRow {
_init(name) {
super._init({ name });
const box = new Gtk.Box({
spacing: 12,
margin_top: 6,
margin_bottom: 6,
margin_start: 6,
margin_end: 6,
});
const label = new Gtk.Label({
hexpand: true,
xalign: 0,
max_width_chars: 25,
ellipsize: Pango.EllipsizeMode.END,
});
this.bind_property('name', label, 'label',
GObject.BindingFlags.SYNC_CREATE);
box.append(label);
const button = new Gtk.Button({
action_name: 'workspaces.remove',
action_target: new GLib.Variant('s', name),
icon_name: 'edit-delete-symbolic',
has_frame: false,
});
box.append(button);
this._entry = new Gtk.Entry({
max_width_chars: 25,
});
const controller = new Gtk.ShortcutController();
controller.add_shortcut(new Gtk.Shortcut({
trigger: Gtk.ShortcutTrigger.parse_string('Escape'),
action: Gtk.CallbackAction.new(() => {
this._stopEdit();
return true;
}),
}));
this._entry.add_controller(controller);
this._stack = new Gtk.Stack();
this._stack.add_named(box, 'display');
this._stack.add_named(this._entry, 'edit');
this.child = this._stack;
this._entry.connect('activate', () => {
this.name = this._entry.text;
this._stopEdit();
});
this._entry.connect('notify::has-focus', () => {
if (this._entry.has_focus)
return;
this._stopEdit();
});
this.connect('notify::name', () => {
button.action_target = new GLib.Variant('s', this.name);
this.activate_action('workspaces.update', null);
});
}
edit() {
this._entry.text = this.name;
this._entry.grab_focus();
this._stack.visible_child_name = 'edit';
}
_stopEdit() {
this.grab_focus();
this._stack.visible_child_name = 'display';
}
});
const NewWorkspaceRow = GObject.registerClass(
class NewWorkspaceRow extends Adw.PreferencesRow {
_init() {
super._init({
action_name: 'workspaces.add',
child: new Gtk.Image({
icon_name: 'list-add-symbolic',
pixel_size: 16,
margin_top: 12,
margin_bottom: 12,
margin_start: 12,
margin_end: 12,
}),
});
this.update_property(
[Gtk.AccessibleProperty.LABEL], [_('Add Workspace')]);
}
});
/** */
function init() {
ExtensionUtils.initTranslations();
Convenience.initTranslations();
}
/**
* @returns {Gtk.Widget} - the prefs widget
*/
function buildPrefsWidget() {
return new WorkspaceSettingsWidget();
let widget = new WorkspaceSettingsWidget();
widget.show_all();
return widget;
}

View File

@@ -1,28 +1,5 @@
.panel-workspace-indicator {
padding: 0 8px;
}
.panel-workspace-indicator-box {
padding: 4px 0;
spacing: 4px;
}
.panel-workspace-indicator-box .workspace {
width: 40px;
border: 2px solid #000;
border-radius: 2px;
background-color: #595959;
}
.panel-workspace-indicator-box .workspace.active {
border-color: #fff;
}
.workspace-indicator-window-preview {
background-color: #bebebe;
border: 1px solid #828282;
}
.workspace-indicator-window-preview.active {
background-color: #d4d4d4;
padding: 0 8px;
background-color: rgba(200, 200, 200, .5);
border: 1px solid #cccccc;
}

View File

@@ -15,7 +15,7 @@ and will be picked automatically at next login.
<homepage rdf:resource="https://wiki.gnome.org/Projects/GnomeShell/Extensions" />
<mailing-list rdf:resource="http://mail.gnome.org/mailman/listinfo/gnome-shell-list" />
<download-page rdf:resource="http://download.gnome.org/sources/gnome-shell-extensions/" />
<bug-database rdf:resource="https://gitlab.gnome.org/GNOME/gnome-shell-extensions/issues" />
<bug-database rdf:resource="http://bugzilla.gnome.org/browse.cgi?product=gnome-shell&amp;component=extensions" />
<category rdf:resource="http://api.gnome.org/doap-extensions#core" />
<programming-language>JavaScript</programming-language>
@@ -34,10 +34,4 @@ and will be picked automatically at next login.
<gnome:userid>fmuellner</gnome:userid>
</foaf:Person>
</maintainer>
<maintainer>
<foaf:Person>
<foaf:name>Marge Bot</foaf:name>
<gnome:userid>marge-bot</gnome:userid>
</foaf:Person>
</maintainer>
</Project>

12
include.mk Normal file
View File

@@ -0,0 +1,12 @@
extensionurl = http://git.gnome.org/gnome-shell-extensions
# Change these to modify how installation is performed
topextensiondir = $(datadir)/gnome-shell/extensions
extensionbase = @gnome-shell-extensions.gcampax.github.com
gschemabase = org.gnome.shell.extensions
uuid = $(EXTENSION_ID)$(extensionbase)
gschemaname = $(gschemabase).$(EXTENSION_ID)
extensiondir = $(topextensiondir)/$(uuid)

93
lib/convenience.js Normal file
View File

@@ -0,0 +1,93 @@
/* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */
/*
Copyright (c) 2011-2012, Giovanni Campagna <scampa.giovanni@gmail.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the GNOME nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
const Gettext = imports.gettext;
const Gio = imports.gi.Gio;
const Config = imports.misc.config;
const ExtensionUtils = imports.misc.extensionUtils;
/**
* initTranslations:
* @domain: (optional): the gettext domain to use
*
* Initialize Gettext to load translations from extensionsdir/locale.
* If @domain is not provided, it will be taken from metadata['gettext-domain']
*/
function initTranslations(domain) {
let extension = ExtensionUtils.getCurrentExtension();
domain = domain || extension.metadata['gettext-domain'];
// check if this extension was built with "make zip-file", and thus
// has the locale files in a subfolder
// otherwise assume that extension has been installed in the
// same prefix as gnome-shell
let localeDir = extension.dir.get_child('locale');
if (localeDir.query_exists(null))
Gettext.bindtextdomain(domain, localeDir.get_path());
else
Gettext.bindtextdomain(domain, Config.LOCALEDIR);
}
/**
* getSettings:
* @schema: (optional): the GSettings schema id
*
* Builds and return a GSettings schema for @schema, using schema files
* in extensionsdir/schemas. If @schema is not provided, it is taken from
* metadata['settings-schema'].
*/
function getSettings(schema) {
let extension = ExtensionUtils.getCurrentExtension();
schema = schema || extension.metadata['settings-schema'];
const GioSSS = Gio.SettingsSchemaSource;
// check if this extension was built with "make zip-file", and thus
// has the schema files in a subfolder
// otherwise assume that extension has been installed in the
// same prefix as gnome-shell (and therefore schemas are available
// in the standard folders)
let schemaDir = extension.dir.get_child('schemas');
let schemaSource;
if (schemaDir.query_exists(null))
schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
GioSSS.get_default(),
false);
else
schemaSource = GioSSS.get_default();
let schemaObj = schemaSource.lookup(schema, true);
if (!schemaObj)
throw new Error('Schema ' + schema + ' could not be found for extension '
+ extension.metadata.uuid + '. Please check your installation.');
return new Gio.Settings({ settings_schema: schemaObj });
}

Some files were not shown because too many files have changed in this diff Show More