Настройка автосохранения в NC 23+

На работе возник вопрос "куда наник выполняет автосохранение?" А, и заодно - формат файла по умолчанию поменять бы на Dwg2013. Клиентов энное количество, к каждому не набегаешься.

Да ну, чего там сложного - подумал я. И, как обычно, ошибся ;)

Все проворачиваю на NC23 (на ноуте установлен только он), хотя технология отлично сработала и на 23.1.

Рассуждения о настройках по умолчанию
Скрин прост и незатейлив:
2025-04-03_10-54-27
Все, что выделено в рамочке, и буду ковырять.

Но предварительно хотелось бы рассмотреть настройки папок автосохранения, резервной копии, файлов истории. По умолчанию там стоят пустые строки. Что сие значит?

Насколько я понимаю, при таких значениях наник пытается сохранить сооответствующие файлы рядом с открытым dwg. Но вот если права на запись в каталог нету, то все эти баки и автосейвы кидаются… В %temp%? Ну да, под Windows – скорее всего именно туда. А вот под Linux место хранения может гулять как угодно. Лично наблюдал, как автосохранение отправлялось в c:\users\root\tmp (путь стырен из ком.строки наника, так что не удивляемся). Хотя логин пользователя далеко не root. Так что лучше бы эти настройки поменять на что-то более вменяемое.

Нашел только переменные по истории файлов, длительности истории и количества версий файла. В остальном никаких системных переменных я не нашел; лисповые getenv / setenv в нанике работают странновато по сравнению с акадом. Так что придется лезьть напрямую в реестр, и править все именно там.

И тут есть проблема: допустим, я поменял каталоги автосохранения & Co. А читает ли наник эти настройки каждый раз, как ему приходится выполнять автосохранение? ИМХО лучше всего не экспериментировать на ровном месте и просто просить пользователя после смены каталогов перезапустить наник.

Попытки кодирования
Проектик будет практически учебным, и делить его на несколько библиотек пока что не планирую. Да и тесты писать не буду, нафиг. Интерфейсы, абстрактные классы и прочая муть отправляются далеко и без хлеба.

Для начала создам-ка несколько перечислений – для формата файла, для настроек инкрементального сохранения, и, пожалуй, для “применять выбранный формат”. Перечисления привожу чохом, хотя в проекте, конечно, они разнесены по разным файлам.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
using System.ComponentModel;

namespace NCadSave.Infrastructure.Enums
{
    /// <summary>Формат для сохранения файла</summary>
    public enum DwgFormatForSave
    {
        [Description("Dwg 2018")]
        Dwg2018 = 1,
        [Description("Dwg 2013")]
        Dwg2013 = 2,
        [Description("Dwg 2010")]
        Dwg2010 = 3,
        [Description("Dwg 2007")]
        Dwg2007 = 4,
        [Description("Dwg 2004")]
        Dwg2005,
        [Description("Dwg 2000")]
        Dwg2000 = 6,
    }

    /// <summary>Режим инкрементального сохранения</summary>
    public enum IncrementalSaveMode
    {
        [Description("Отключено")]
        Off = 0,
        [Description("При сохранении")]
        OnSave = 1,
        [Description("При автосохранении и сохранении")]
        OnSaveAndAutoSave = 2,
    }
    /// <summary>Использование выбранного формата хранения</summary>
    public enum UseFormat
    {
        [Description("Не использовать")]
        Off = 0,
        [Description("Для новых документов")]
        NewDocuments = 1,
        [Description("Для всех документов")]
        AllDocuments = 2,
    }
}[//cc]

Тут получилось очень забавно с форматами файлов. Похоже, нумерация идет как в выпадающем списке, и начинается с 1. Только из-за этого и пришлось прописывать значения для перечисления. Остальные, можно сказать, уже по инерции назначил ;)

Теперь делаю класс собственно настроек:
[cc lang="csharp"]using NCadSave.Infrastructure.Enums;

namespace NCadSave.Infrastructure
{
    /// <summary>Настройки, относящиеся к хранению файлов в NCad</summary>
    public class SaveSettings
    {
        /// <summary>Режим инкрементального сохранения</summary>
        public IncrementalSaveMode IncSaveMode { get; set; }
        /// <summary>Формат по умолчанию для сохранения файла</summary>
        public DwgFormatForSave DefaultFormat { get; set; }
        /// <summary>Применять выбранныйй формат <see cref="DefaultFormat"/> к документам...</summary>
        public UseFormat UseDefaultFormat { get; set; }
        /// <summary>Каталог автосохранения</summary>
        public string AutosaveFolder { get; set; }
        /// <summary>Каталог с файлами историй</summary>
        public string HistoryFolder { get; set; }
        /// <summary>Время автосохранения</summary>
        public byte AutosaveTimeout { get; set; }
        /// <summary>Создавать bak</summary>
        public bool CreateBak { get; set; }
        /// <summary>Создавать bak оригинала</summary>
        public bool CreateOriginalBak { get; set; }

    }
}

Теперь работа с настройками )) Поскольку работа будет с реестром, в конструкторе будет параметр – имя ветки реестра, внутри которой в общем-то все и будет болтаться. Чтение настроек можно сделать как в классе, так и отдельным методом. Сделаю, пожалуй, отдельным методом – и в таком случае у класса будет 3 метода: чтение, запись, сброс до рекомендованных значений. Пока ставлю на них заглушки, а “рекомендованные значения” приколочу гвоздями в коде: прописывать еще и сериализацию откровенно лень.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
namespace NCadSave.Infrastructure
{
    public class NanoCadSettings
    {
        /// <summary>Настройик NCad.  В текущих реалиях - только касаемо автосохранения</summary>
        /// <param name="registryHiveName">Путь к разделу реестр, в конце обязательно должен включать подкаталог Profiles\ProfileName</param>
        public NanoCadSettings(string registryHiveName)
        {
            RegistryHiveName = registryHiveName;
        }

        /// <summary>Чтение имеющихся настроек nanoCAD из реестра</summary>
        /// <returns></returns>
        /// <exception cref="NotImplementedException"></exception>
        public SaveSettings GetSaveSettings()
        {
            throw new NotImplementedException();
        }

        /// <summary>Сохранение настроек nanoCAD</summary>
        /// <param name="settings"></param>
        /// <exception cref="NotImplementedException"></exception>
        public void KeepSaveSettings(SaveSettings settings)
        {
            throw new NotImplementedException ();
        }

        /// <summary>Восстановить рекомендованные значения</summary>
        /// <exception cref="NotImplementedException"></exception>
        public void ResetToRecommendedSaveSettings()
        {
            throw new NotImplementedException ();
        }

        public string RegistryHiveName { get; }
    }
}

Сначала, пожалуй, сделаю чтение. Пока писал, понял, что забыл про каталог страховых копий. В репозитории все есть, так что здесь править не буду ;)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>Чтение имеющихся настроек nanoCAD из реестра</summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public SaveSettings GetSaveSettings()
{
    SaveSettings result = new SaveSettings();
    RegistryKey hkcu = Registry.CurrentUser;

    using (RegistryKey hive = hkcu.OpenSubKey(_autosaveFolderHive)) {
        result.AutosaveFolder = hive.GetValue(_autosaveFolderKey).ToString();
        result.BackupFolder = hive.GetValue(_backupFolderKey).ToString();
    }

    using (RegistryKey hive = hkcu.OpenSubKey(_historyFolderHive))
    {
        result.HistoryFolder = hive.GetValue(_historyFolderKey).ToString();
    }

    throw new NotImplementedException();
}

Я ввел приватные переменные, поскольку по тем же путям и по тем же ключам буду записывать данные. Минимум два прохода будет ))
Теперь добавляю чтение формата, режима инкрементального сохранения, режима применения формата. Заодно и каталоги прочитаем ;)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/// <summary>Чтение имеющихся настроек nanoCAD из реестра</summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public SaveSettings GetSaveSettings()
{
    SaveSettings result = new SaveSettings();
    RegistryKey hkcu = Registry.CurrentUser;

    using (RegistryKey hive = hkcu.OpenSubKey(_autosaveFolderHive)) {
        result.AutosaveFolder = hive.GetValue(_autosaveFolderKey).ToString();
        result.BackupFolder = hive.GetValue(_backupFolderKey).ToString();
    }

    using (RegistryKey hive = hkcu.OpenSubKey(_historyFolderHive))
    {
        result.HistoryFolder = hive.GetValue(_historyFolderKey).ToString();
    }

    using (RegistryKey hive = hkcu.OpenSubKey(_saveProjectsHive))
    {
        byte[] format = (byte[])hive.GetValue(_defaultFormatKey);
        result.DefaultFormat = (DwgFormatForSave)format[4];

        byte[] saveMode = (byte[]) hive.GetValue(_incSaveModeKey);
        result.IncSaveMode = (IncrementalSaveMode)saveMode[4];

        byte[] useFormat = (byte[])hive.GetValue(_useSaveFormatKey);
        result.UseDefaultFormat = (UseFormat)useFormat[4];
    }

    throw new NotImplementedException();
}

По аналогии – таймаут сохранения, создание баков и прочая, и прочая (текста и так много, так что ограничусь только повторением работы с веткой автосохранения):

1
2
3
4
5
6
7
8
9
10
11
12
13
using (RegistryKey hive = hkcu.OpenSubKey(_autosaveFolderHive)) {
    result.AutosaveFolder = hive.GetValue(_autosaveFolderKey).ToString();
    result.BackupFolder = hive.GetValue(_backupFolderKey).ToString();

    byte[] timeout = (byte[]) hive.GetValue(_timeoutKey);
    result.AutosaveTimeout = timeout[4];

    byte[] createBak = (byte[])hive.GetValue(_createBakKey);
    result.CreateBak = createBak[4] == 1;

    byte[] createOrigBak = (byte[])hive.GetValue(_createOrigBakKey);
    result.CreateOriginalBak = createOrigBak[4] == 1;
}

Теперь запись? Ну, почти. NC в смысле каталогов ведет себя очень забавно. Можно руками вколотить любой путь к папке, к примеру, автосохранения. Но если этого каталога нет, наник начинает вести себя так, будто там пустая строка, и каталог автоматом не создает. Так что помимо сохранения данных каталоги еще и создавать придется.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/// <summary>Сохранение настроек nanoCAD</summary>
/// <param name="settings"></param>
public void KeepSaveSettings(SaveSettings settings)
{
    if (!string.IsNullOrEmpty(settings.AutosaveFolder) && !Directory.Exists(settings.AutosaveFolder))
    {
        Directory.CreateDirectory(settings.AutosaveFolder);
    }
    if (!string.IsNullOrEmpty(settings.BackupFolder) && !Directory.Exists(settings.BackupFolder))
    {
        Directory.CreateDirectory(settings.BackupFolder);
    }
    if (!string.IsNullOrEmpty(settings.HistoryFolder) && !Directory.Exists(settings.HistoryFolder))
    {
        Directory.CreateDirectory(settings.HistoryFolder);
    }
    #region Реестр
    RegistryKey hkcu = Registry.CurrentUser
    using (RegistryKey hive = hkcu.OpenSubKey(_autosaveFolderHive, true))
    {
        hive.SetValue(_autosaveFolderKey, settings.AutosaveFolder);
        hive.SetValue(_backupFolderKey, settings.BackupFolder);
        byte[] timeout = (byte[])hive.GetValue(_timeoutKey;
        timeout[4] = settings.AutosaveTimeout;

        hive.SetValue(_timeoutKey, timeout);
        byte[] createBak = (byte[])hive.GetValue(_createBakKey);
        createBak[4] = (byte)(settings.CreateBak ? 1 : 0);

        hive.SetValue(_createBakKey, createBak);
        byte[] createOrigBak = (byte[])hive.GetValue(_createOrigBakKey);
        createOrigBak[4] = (byte)(settings.CreateOriginalBak ? 1 : 0);
        hive.SetValue(_createOrigBakKey, createOrigBak);
    }

    using (RegistryKey hive = hkcu.OpenSubKey(_historyFolderKey, true))
    {
        hive.SetValue(_historyFolderHive, settings.HistoryFolder);
    }

    using (RegistryKey hive = hkcu.OpenSubKey(_saveProjectsHive, true))
    {
        byte[] format = (byte[]) hive.GetValue(_defaultFormatKey);
        format[4]=(byte)settings.DefaultFormat;
        hive.SetValue(_defaultFormatKey, format);

        byte[] saveMode = (byte[])hive.GetValue(_incSaveModeKey);
        saveMode[4] = (byte)settings.IncSaveMode;
        hive.SetValue(_incSaveModeKey, saveMode);

        byte[] useFormat = (byte[])hive.GetValue(_useSaveFormatKey);
        useFormat[4] = (byte)settings.UseDefaultFormat;
        hive.SetValue(_useSaveFormatKey, useFormat);
    }
    #endregion
}

Вроед ничего не упустил. Так что добавляю вариант “приколочено гвоздями”. Чтоб не сходить с ума потом, делаю все же 2 метода: один просто будет возвращать “рекомендованные” значения, второй – сохранять.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>Рекомендованные значения</summary>
/// <returns></returns>
public SaveSettings GetRecommendedSaveSettings()
{
    return new SaveSettings()
    {
        AutosaveTimeout = 20,
        AutosaveFolder = Path.Combine(_customFolder, "Autosave"),
        BackupFolder = Path.Combine(_customFolder, "Backups"),
        CreateBak = true,
        CreateOriginalBak = false,
        DefaultFormat = DwgFormatForSave.Dwg2013,
        HistoryFolder = Path.Combine("History"),
        IncSaveMode = IncrementalSaveMode.Off,
        UseDefaultFormat = UseFormat.AllDocuments
    };
}

/// <summary>Восстановить рекомендованные значения</summary>
public void ResetToRecommendedSaveSettings()
{
    SaveSettings settings = GetRecommendedSaveSettings();
    KeepSaveSettings(settings);
}

Работу с системными переменными прописывать нет никакого желания, – FILEHISTORY, FILEHISTORYDURATION, FILEHISTORYMAX и так можно будет потом дообработать ;)

Осталось теперь как минимум “приколоченный” вариант воткнуть в наник, да? В принципе да – достаточно прочитать данные и сравнить каталоги, чтоб понять, надо вываливать сообщение пользователю или нет.

С одной стороны, можно сделать команду, с другой – внедрить в инициализатор, с третьей это можно объединить. Основная проблема – это сравнить старые и новые данные. Мне кажется, самое простое – это просто реализовать возможность сравнения напрямую в SaveSettings. Тогда можно будет прочитать имеющиеся настройки, получить “рекомендованные”, сравнить, чего-то сделать…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using NCadSave.Infrastructure.Enums;

namespace NCadSave.Infrastructure
{
    /// <summary>Настройки, относящиеся к хранению файлов в NCad</summary>
    public class SaveSettings : IEquatable<SaveSettings>
    {
        /// <summary>Режим инкрементального сохранения</summary>
        public IncrementalSaveMode IncSaveMode { get; set; }
        /// <summary>Формат по умолчанию для сохранения файла</summary>
        public DwgFormatForSave DefaultFormat { get; set; }
        /// <summary>Применять выбранныйй формат <see cref="DefaultFormat"/> к документам...</summary>
        public UseFormat UseDefaultFormat { get; set; }
        /// <summary>Каталог автосохранения</summary>
        public string AutosaveFolder { get; set; }
        /// <summary>Каталог резервных копий</summary>
        public string BackupFolder { get; set; }
        /// <summary>Каталог с файлами историй</summary>
        public string HistoryFolder { get; set; }
        /// <summary>Время автосохранения</summary>
        public byte AutosaveTimeout { get; set; }
        /// <summary>Создавать bak</summary>
        public bool CreateBak { get; set; }
        /// <summary>Создавать bak оригинала</summary>
        public bool CreateOriginalBak { get; set; }

        public bool Equals(SaveSettings? other)
        {
            if (ReferenceEquals(null, other)) return false;
            if (ReferenceEquals (this, other)) return true;
            return AutosaveFolder.Equals(other.AutosaveFolder, StringComparison.InvariantCultureIgnoreCase)
                && BackupFolder.Equals(other.BackupFolder, StringComparison.InvariantCultureIgnoreCase)
                && HistoryFolder.Equals(other.HistoryFolder, StringComparison.InvariantCultureIgnoreCase);
        }

        public override bool Equals(object? obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != typeof (SaveSettings)) return false;
            return Equals((SaveSettings)obj);
        }

        public override int GetHashCode()
        {
            var hashCode = (HistoryFolder != null ? HistoryFolder.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (BackupFolder != null ? BackupFolder.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ (AutosaveFolder != null ? AutosaveFolder.GetHashCode() : 0);
            return hashCode;
        }
    }
}

В условиях Linux, учитывая, что NC работает под Wine, можно сравнивать строки с игнорированием регистра (хотя в Linux, по-моему, каталоги Abc и aBc могут вполне легко сосуществовать рядом). В общем, пока что оставляю как есть. С надеждой, что переделывать не понадобится ;)

Теперь можно и команду прописывать:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using HostMgd.ApplicationServices;
using NCadSave.Infrastructure;
using System.IO;
using System.Windows;
using Teigha.Runtime;
using Application = HostMgd.ApplicationServices.Application;
using MessageBox = System.Windows.MessageBox;

namespace NCadSave.NCadCommands
{
    public class SetSaveSettingsCmd
    {
        [CommandMethod("kpblc-set-savesettings")]
        public static void SetSaveSettingsCommand()
        {
            string regName = Path.Combine(Teigha.DatabaseServices.HostApplicationServices.Current.UserRegistryProductRootKey,
                "Profiles", Application.GetSystemVariable("cprofile").ToString());
            NanoCadSettings settings = new NanoCadSettings(regName);
            SaveSettings existData = settings.GetSaveSettings();
            settings.ResetToRecommendedSaveSettings();
            if (existData != settings.GetRecommendedSaveSettings())
            {
                string message = "Были изменены каталоги в настройках. Настоятельно рекомендуется перезапустить nanoCAD!";

                MessageBox.Show(message, "Внимание!", MessageBoxButton.OK, MessageBoxImage.Hand);

                Document doc = Application.DocumentManager.MdiActiveDocument;
                if (doc != null)
                {
                    doc.Editor.WriteMessage($"\n{message}");
                }
            }
        }
    }
}

Можно теперь и проверить, чего получится. По ходу дела подправить пару опечаток и одну логическую ошибку, и…

Начальные настройки:
2025-04-03_15-04-43
После вызова команды kpblc-set-savesettings выводится сообщение:
2025-04-03_15-09-24
Ну и в результате
2025-04-03_15-09-48

Ну, почти. Каталог с историей какой-то странненький. Правильно, забыл его корректно прописать в рекомендованных значениях:

1
HistoryFolder = Path.Combine(_customFolder, "History"),

Похоже, все правильно. В ExtennsionInitialize прописывать ничего не буду )))

Реп: https://github.com/kpblc2000/NCadSaveSettings

В принципе, все кроме собственно команды, можно “вытащить” в отдельную сборку NET Standard и подключать ее по мере надобности как к NET, так и к Framework-проектам.



Поделитесь своим мнением


Я не робот.