Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ You can choose the images following case.
If this option is enabled, Screencast Keys will be enabled automatically when
blender will startup up.

### Persist Preferences
If this option is enabled, Screencast Keys will store its preferences
in a local file and will restore them when reloaded.
The preferences are stored in blender's user configuration directory: `config/screencast_keys/preferences.json`

### UI

#### Sidebar
Expand Down
21 changes: 7 additions & 14 deletions src/screencast_keys/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,28 +153,17 @@ def register():
context = bpy.context
user_prefs = context.preferences
prefs = user_prefs.addons[__package__].preferences
preferences.load_persisted_preferences(prefs)
# Only default panel location is available in < 2.80
if utils.compatibility.check_version(2, 80, 0) < 0:
prefs.panel_space_type = 'VIEW_3D'
prefs.panel_category = "Screencast Key"
prefs.show_ui_in_sidebar = True
prefs.show_ui_in_overlay = False
preferences.ensure_display_event_text_aliases(prefs)
preferences.SK_Preferences.ui_in_sidebar_update_fn(prefs, context)
preferences.SK_Preferences.ui_in_overlay_update_fn(prefs, context)

# Fix: https://github.com/nutti/Screencast-Keys/issues/195

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is my understanding correct, this is still valid when there is no configuration file,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I tested and there's no traces of that.

The fix is now part of the function def ensure_display_event_text_aliases(prefs) in preferences.py.

This will create a dictionary containing all the known keys taking from ops.EventType.names[event.name] same ones used by default and guarantees a single entry per item.

Then it reads from the user overrides which don't need to be persisted on the save file for this to work, keeping the fix.

# TODO: This workaround is hacky. We need to apply a better solution.
if len(prefs.display_event_text_aliases_props) == 0:
for event in list(ops.EventType):
item = prefs.display_event_text_aliases_props.add()
item.event_id = event.name
if event in ops.SK_OT_ScreencastKeys.MODIFIER_EVENT_TYPES:
item.default_text = common.fix_modifier_display_text(
ops.EventType.names[event.name]
)
else:
item.default_text = ops.EventType.names[event.name]

try:
common.reload_custom_mouse_image(prefs, context)
# pylint: disable=W0702
Expand All @@ -187,7 +176,11 @@ def unregister():
user_prefs = context.preferences
prefs = user_prefs.addons[__package__].preferences
preferences.remove_custom_mouse_image(prefs, context)
prefs.display_event_text_aliases_props.clear()
if prefs.persist_preferences:
preferences.save_persisted_preferences(prefs)
else:
preferences.remove_persisted_preferences_file()
prefs.display_event_text_aliases_props.clear()

bpy.app.handlers.load_post.remove(load_post_handler)
unregister_shortcut_key()
Expand Down
3 changes: 3 additions & 0 deletions src/screencast_keys/blender_manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ blender_version_min = "4.2.0"
license = [
"SPDX:GPL-3.0-or-later",
]

[permissions]
files = "Save settings to a file and restore/delete them according to preferences"
172 changes: 172 additions & 0 deletions src/screencast_keys/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
# <pep8 compliant>


import json
import os

import bpy
from bpy.props import (
StringProperty,
Expand All @@ -32,6 +35,22 @@
from .utils.bl_class_registry import BlClassRegistry
from . import common
from . import c_structure as cstruct # extensions.blender.org: Delete line
from . import ops


PERSISTED_PREFERENCE_EXCLUDES = {
"bl_idname",
"updater_branch_to_update",
}

PERSISTED_PREFERENCE_TYPES = {
'BOOLEAN',
'COLLECTION',
'ENUM',
'FLOAT',
'INT',
'STRING',
}


# extensions.blender.org: Delete block start
Expand Down Expand Up @@ -132,6 +151,151 @@ class DisplayEventTextAliasProperties(bpy.types.PropertyGroup):
event_id: bpy.props.StringProperty(options={'HIDDEN'})


def persisted_preferences_filepath(create=False):
config_dir = bpy.utils.user_resource(
'CONFIG', path="screencast_keys", create=create)
if not config_dir:
return ""

return os.path.join(config_dir, "preferences.json")


def _rna_properties(data):
for prop in data.bl_rna.properties:
if prop.identifier == "rna_type":
continue
if prop.identifier in PERSISTED_PREFERENCE_EXCLUDES:
continue
if prop.type not in PERSISTED_PREFERENCE_TYPES:
continue
if prop.is_readonly and prop.type != 'COLLECTION':
continue
yield prop


def _serialize_property(data, prop):
value = getattr(data, prop.identifier)
if prop.type == 'COLLECTION':
return [_serialize_preferences(item) for item in value]
if getattr(prop, "array_length", 0) > 0:
return list(value)

return value


def _serialize_preferences(data):
preferences = {}
for prop in _rna_properties(data):
try:
preferences[prop.identifier] = _serialize_property(data, prop)
except TypeError:
continue

return preferences


def _apply_preferences(data, preferences):
properties = {
prop.identifier: prop
for prop in _rna_properties(data)
}
for identifier, value in preferences.items():
if identifier not in properties:
continue

prop = properties[identifier]
if prop.type == 'COLLECTION':
if not isinstance(value, list):
continue
collection = getattr(data, identifier)
collection.clear()
for item_data in value:
if not isinstance(item_data, dict):
continue
item = collection.add()
_apply_preferences(item, item_data)
else:
try:
data[identifier] = value
except (AttributeError, KeyError, TypeError, ValueError):
try:
setattr(data, identifier, value)
except (TypeError, ValueError):
continue


def ensure_display_event_text_aliases(prefs):


event_aliases = {
item.event_id: item
for item in prefs.display_event_text_aliases_props
}
for event in list(ops.EventType):
if event in ops.SK_OT_ScreencastKeys.MODIFIER_EVENT_TYPES:
default_text = common.fix_modifier_display_text(
ops.EventType.names[event.name]
)
else:
default_text = ops.EventType.names[event.name]

if event.name in event_aliases:
event_aliases[event.name].default_text = default_text
else:
item = prefs.display_event_text_aliases_props.add()
item.event_id = event.name
item.default_text = default_text


def load_persisted_preferences(prefs):
filepath = persisted_preferences_filepath()
if not filepath or not os.path.exists(filepath):
return

try:
with open(filepath, "r", encoding="utf-8") as file:
data = json.load(file)
except (OSError, json.JSONDecodeError):
return

if not data.get("persist_preferences", False):
return

_apply_preferences(prefs, data.get("preferences", {}))
prefs.persist_preferences = True


def save_persisted_preferences(prefs):
filepath = persisted_preferences_filepath(create=True)
if not filepath:
return

data = {
"version": "1.0.0",
"persist_preferences": True,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this item mean?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the entry point for the persist Boolean of the new setting. It has to be written explicitly the first time a preferences file is created to keep track if the user want to restore this setting file or delete it. Maybe I can think of aa different name.

"preferences": _serialize_preferences(prefs),
}

try:
tmp_filepath = "{}.tmp".format(filepath)
with open(tmp_filepath, "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=2)
os.replace(tmp_filepath, filepath)
except (OSError, TypeError):
return


def remove_persisted_preferences_file():
filepath = persisted_preferences_filepath()
if not filepath:
return

try:
os.remove(filepath)
except OSError:
return


# pylint: disable=W0613
def remove_custom_mouse_image(self, _):
def remove_image(image_name):
Expand Down Expand Up @@ -525,6 +689,13 @@ def ui_in_overlay_update_fn(self, _):
update=ui_in_overlay_update_fn,
)

persist_preferences: bpy.props.BoolProperty(
name="Persist Preferences",
description="Store add-on preferences in Blender user configuration "
"and restore them when this add-on is enabled again",
default=False,
)

display_event_text_aliases_props: bpy.props.CollectionProperty(
type=DisplayEventTextAliasProperties
)
Expand All @@ -547,6 +718,7 @@ def draw(self, _):
layout.separator()

layout.prop(self, "enable_on_startup")
layout.prop(self, "persist_preferences")

column = layout.column()
split = column.split()
Expand Down