Skip to content

Commit

Permalink
Merge pull request #985 from szos/mixin-modes-no-deps
Browse files Browse the repository at this point in the history
Implement Minor Modes as Mixins
  • Loading branch information
dmb2 committed Jun 26, 2022
2 parents 56a71bd + c30d328 commit 189a730
Show file tree
Hide file tree
Showing 22 changed files with 2,218 additions and 208 deletions.
2 changes: 1 addition & 1 deletion Makefile.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ infodir=@infodir@

# You shouldn't have to edit past this

FILES=stumpwm.asd $(shell grep -o ":file \".*\"" stumpwm.asd | sed 's,:file ",,g' | sed 's,",.lisp,g' )
FILES=stumpwm.asd $(shell grep -o ":file \".*\"" stumpwm.asd | sed 's,:file ",,g' | sed 's,",.lisp,g' ) dynamic-mixins/dynamic-mixins.asd $(shell grep -o ":file \".*\"" dynamic-mixins/dynamic-mixins.asd | sed 's,:file ",dynamic-mixins/src/,g' | sed 's,",.lisp,g')

all: stumpwm stumpwm.info

Expand Down
9 changes: 5 additions & 4 deletions command.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,14 @@ whatever it finds: a command, an alias, or nil."

(defun command-active-p (command)
(declare (special *dynamic-group-blacklisted-commands*))
(let ((active (typep (current-group) (command-class command))))
(let* ((group (current-group))
(active (or (typep group (command-class command))
(some (lambda (f) (funcall f group command))
*custom-command-filters*))))
(if (typep (current-group) 'dynamic-group)
(unless (member command *dynamic-group-blacklisted-commands*)
active)
active))
;; TODO: minor modes
)
active)))

(defun get-command-structure (command &optional (only-active t))
"Return the command structure for COMMAND. COMMAND can be a string,
Expand Down
40 changes: 29 additions & 11 deletions dynamic-group.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,13 @@
;; The window definition remains unchanged, as at its core it is a tile
;; window. All we do is add a single tag.

(defclass dynamic-window (tile-window)
(define-swm-class dynamic-window (tile-window)
((superfluous :initform nil
:accessor superfluous-window-tag)))

(defmethod print-swm-object ((object dynamic-window) stream)
(format stream "DYNAMIC-WINDOW ~s #x~x" (window-name object) (window-id object)))

(defmethod superfluous-window-p ((window dynamic-window))
(superfluous-window-tag window))

Expand All @@ -84,7 +87,7 @@
;; policy to live at the class level and add a head placement policy to
;; determine where new windows should be placed.

(defclass dynamic-group (tile-group)
(define-swm-class dynamic-group (tile-group)
(;; Class allocated slots
(head-placement-policy
:reader dynamic-group-head-placement-policy
Expand Down Expand Up @@ -130,6 +133,9 @@ stack windows, and SEVENTH is the major split ratio."))
(:documentation "A group type that implements dynamic tiling à la DWM with a
single master window and a window stack."))

(defmethod print-swm-object ((object dynamic-window) stream)
(format stream "DYNAMIC-WINDOW ~s #x~x" (window-name object) (window-id object)))

(defun dynamic-group-p (thing)
(typep thing 'dynamic-group))

Expand Down Expand Up @@ -432,14 +438,17 @@ return NIL. RATIO is a fraction to split by."
(cond ((typep window 'float-window)
(call-next-method))
((eq frame :float)
(change-class window 'float-window)
(dynamic-mixins:replace-class window 'float-window)
(float-window-align window)
(sync-minor-modes window)
(when raise (group-focus-window group window)))
(t ; if were not dealing with a floating window
(let ((head (choose-head-from-placement-policy group)))
;; keep all calls to change-class in the same place.x
(change-class window 'dynamic-window)
(dynamic-group-add-window group head window)))))
(dynamic-mixins:replace-class window 'dynamic-window)
;; (change-class window 'dynamic-window)
(dynamic-group-add-window group head window)
(sync-minor-modes window)))))

(defmethod group-delete-window ((group dynamic-group) (window dynamic-window))
"Delete a dynamic window from a dynamic group. For floating windows we fall
Expand Down Expand Up @@ -702,13 +711,18 @@ floating windows onto the stack."
(append
(loop for w in (head-windows group head)
when (float-window-p w)
collect (change-class w 'dynamic-window))
collect w)
stack-windows)
stack-windows)))))
(setf master-window nil
stack-windows nil)
(loop for window in windows
do (dynamic-group-place-window group head window))
(loop with previous-floats = nil
for window in windows
do (when (float-window-p window)
(push window previous-floats)
(dynamic-mixins:replace-class window 'dynamic-window))
(dynamic-group-place-window group head window)
finally (map nil #'sync-minor-modes window))
(focus-frame group (window-frame master-window))))))

;;; Handle overflow of both heads and groups
Expand Down Expand Up @@ -1014,8 +1028,10 @@ window. "
(message "Window ~A is already a floating window." window)
(progn
(group-delete-window group window)
(change-class window 'float-window)
(dynamic-mixins:replace-class window 'float-window)
;; (change-class window 'float-window)
(float-window-align window)
(sync-minor-modes window)
(focus-all window))))

(defun dynamic-group-unfloat-window (window group)
Expand All @@ -1024,8 +1040,10 @@ window. "
(message "Window ~A is already a dynamic window." window)
(progn
(let ((head (window-head window)))
(change-class window 'dynamic-window)
(dynamic-group-add-window group head window)))))
(dynamic-mixins:replace-class window 'dynamic-window)
;; (change-class window 'dynamic-window)
(dynamic-group-add-window group head window)
(sync-minor-modes window)))))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
78 changes: 78 additions & 0 deletions dynamic-mixins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# dynamic-mixins

Dynamic-mixins is for simple, dynamic class combination:

```lisp
(in-package :dynamic-mixins)
(defclass a () ())
(defclass b () ())
(defclass c () ())
(make-instance (mix 'a 'b)) ;; => #<MIXIN-OBJECT (A B)>
(let ((a (make-instance 'a)))
(ensure-mix a 'b 'c) ;; => #<MIXIN-OBJECT (B C A)>
(delete-from-mix a 'a) ;; => #<MIXIN-OBJECT (B C)>
(delete-from-mix a 'c)) ;; => #<B>
```

This allows objects to be mixed and updated without manually
defining many permutations.

## Dictionary

* `MIX &rest classes`: This produces a "mix
list", which is generally only useful for passing to
`MAKE-INSTANCE`. Note: Order matters! This determines class
precedence.

* `ENSURE-MIX object &rest name-or-class`: Ensure that classes listed
in `name-or-class` are part of `object`. This will create a new
class and `CHANGE-CLASS object` if necessary. Note: Order matters!
This determines class precedence.

* `DELETE-FROM-MIX object &rest name-or-class`: Remove classes listed
in `name-or-class` from the object's class. This will create a new
class and `CHANGE-CLASS object` if necessary. However, `object`
must be a `MIXIN-OBJECT` created by `(MAKE-INSTANCE (MIX ...) ...)`
or `ENSURE-MIX`. Otherwise, nothing will be changed.

## Notes

### Order and Precedence

Order matters; you are defining a new class which has the specified
classes as direct superclasses.

`ENSURE-MIX` *prepends* classes in the order specified. (Originally,
it appended classes.) This is simply more useful in practice:

```lisp
(defclass general-object () ())
(defclass specializing-mixin () ())
(defgeneric some-operation (x))
(defmethod some-operation (x)
"Handle the general case"
...)
(defmethod some-operation ((x specializing-mixin))
"Handle the case for SPECIALIZING-MIXIN"
...)
(let ((x (make-instance 'general-object)))
(ensure-mix x 'specializing-mixin)
(some-operation x))
```

If `SPECIALIZING-MIXIN` were appended, the method which specialized on
it would never be called. In practice, this defeats the point.
Therefore, mixins now get precedence.

### Errors

Errors regarding precendence and circularity are now handled, or
rather, causing such an error will not produce a nearly-unrecoverable
situation. Now you will just get an error.
18 changes: 18 additions & 0 deletions dynamic-mixins/dynamic-mixins.asd
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
(defpackage :dynamic-mixins.asdf
(:use #:cl #:asdf))

(in-package :dynamic-mixins.asdf)

(defsystem :dynamic-mixins
:description "Simple dynamic class mixing without manual permutations"
:author "Ryan Pavlik"
:license "BSD-2-Clause"
:version "0.0"

:depends-on (:alexandria)
:pathname "src"
:serial t

:components
((:file "package")
(:file "dynamic-mixins")))
166 changes: 166 additions & 0 deletions dynamic-mixins/src/dynamic-mixins.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
(in-package :dynamic-mixins)

(defvar *dynamic-mix-classes* (make-hash-table :test 'equal))

(defclass mixin-class (standard-class)
((classes :initform nil :initarg :classes :accessor mixin-classes)))

(defmethod sb-mop:validate-superclass ((class mixin-class) (super standard-class))
t)

(defmethod print-object ((o mixin-class) stream)
(with-slots (classes) o
(print-unreadable-object (o stream :identity t)
(format stream "~S ~S"
(or (class-name o) 'mixin-class)
(mapcar #'class-name classes)))))

(defclass mixin-object () ())

(defstruct mix-list (list nil))

(defun %find-class (name-or-class)
(etypecase name-or-class
(symbol (find-class name-or-class))
(class name-or-class)))

(defun %mix (object-or-class &rest class-list)
"Create a MIX-LIST for MAKE-INSTANCE. The first element may be an
instance; further elements must be class names or classes."
(let ((class0 (typecase object-or-class
(symbol (list (find-class object-or-class)))
(mixin-object
(slot-value (class-of object-or-class) 'classes))
(t (list (class-of object-or-class))))))
(make-mix-list
:list (remove-duplicates
(append (mapcar #'%find-class class-list)
class0)))))

(defun mix (&rest classes)
(make-mix-list :list (remove-duplicates (mapcar #'%find-class classes))))

(defun set-superclasses (class list)
(reinitialize-instance class :direct-superclasses list))

(defun define-mixin (mix-list)
(let ((new-class (make-instance 'mixin-class
:classes (mix-list-list mix-list))))
(handler-case
(progn
(set-superclasses new-class (list* (find-class 'mixin-object)
(mix-list-list mix-list))))
(error (e)
(set-superclasses new-class nil)
(error e)))
(setf (gethash (mix-list-list mix-list) *dynamic-mix-classes*)
new-class)))

(defun ensure-mixin (mix-list)
(if (cdr (mix-list-list mix-list))
(if-let ((class (gethash (mix-list-list mix-list)
*dynamic-mix-classes*)))
class
(define-mixin mix-list))
(car (mix-list-list mix-list))))

(defun ensure-mix (object &rest classes)
(let ((new-class (ensure-mixin (apply #'%mix object classes))))
(change-class object new-class)))

(defun delete-from-mix (object &rest classes)
(if (typep object 'mixin-object)
(let* ((classes (mapcar #'%find-class classes))
(old-classes (slot-value (class-of object) 'classes))
(new-classes (remove-if (lambda (x) (member (%find-class x) classes))
old-classes))
(new-class (if (cdr new-classes)
(ensure-mixin (apply #'mix new-classes))
(car new-classes))))
(change-class object new-class))
object))

(defmethod make-instance ((items mix-list) &rest initargs &key &allow-other-keys)
(apply #'make-instance (ensure-mixin items) initargs))

(defgeneric replace-class-in-mixin (object new-class old-class &rest initargs)
(:method ((object standard-object) n o &rest rest)
(declare (ignore o))
(apply #'change-class object n rest)))

(defmethod replace-class-in-mixin ((object mixin-object)
(new-class class)
(old-class class)
&rest rest)
(apply #'replace-class-in-mixin
object (class-name new-class) (class-name old-class) rest))

(defmethod replace-class-in-mixin ((object mixin-object)
(new-class class)
(old-class symbol)
&rest rest)
(apply #'replace-class-in-mixin object (class-name new-class) old-class rest))

(defmethod replace-class-in-mixin ((object mixin-object)
(new-class symbol)
(old-class class)
&rest rest)
(apply #'replace-class-in-mixin object new-class (class-name old-class) rest))

(defmethod replace-class-in-mixin ((object mixin-object)
(new-class symbol)
(old-class symbol)
&rest initargs)
(cond ((eql new-class old-class)
object)
(t
;; First we disable all non-compatible minor modes.
(loop for mode in (stumpwm::list-minor-modes object)
unless (let* ((scope (stumpwm:minor-mode-scope mode))
(st (stumpwm::scope-type scope)))
(or (eql new-class st)
(stumpwm::superclassp new-class st)))
do (stumpwm::autodisable-minor-mode mode object))
(if (typep object 'mixin-object)
(flet ((mix-it (mix-list)
(apply #'change-class
object (ensure-mixin mix-list) initargs)
(stumpwm::sync-minor-modes object)
object))
(let* ((tag nil)
(old-class-obj (find-class old-class))
(fn (lambda (e)
(when (or (eql e old-class) (eql e old-class-obj))
(setf tag t)
t)))
(mix-list
(make-mix-list
:list (remove-duplicates
(mapcar #'%find-class
(subst-if new-class
fn
(mixin-classes
(class-of object))))))))
(if tag
(mix-it mix-list)
(restart-case
(error "~A is not an explicitly mixed class in ~A"
old-class object)
(continue ()
object)
(mix-in-new-class ()
(ensure-mix object new-class))))))
(apply #'change-class object new-class initargs)))))

(defgeneric replace-class (object new-class &rest initargs))

(defmethod replace-class :around (object new &rest rest)
(restart-case (progn
(call-next-method)
(unless (typep object new)
(error "Failed to change class ~A ~A" object new)))
(force-change ()
:report (lambda (s)
(format s "Change class to ~A, removing all mixins" new))
(apply #'change-class object new rest)))
object)

0 comments on commit 189a730

Please sign in to comment.