Skip to content

Commit ba10100

Browse files
Project settings (#2985)
* Initial creation of settings if .robot directory exists in project dir. * Correct settings to edit, ut always starts with wrong. Ignore command line arguments * Still does not update preferences colors. Needs to reload Text Editor and Test Runner * Improve settings change in Tree when changed project. TODO reload plugins * Fix opening last file, independent of project settings * Initial dialog to restart RIDE, when project settings detected * Working Reload on project settings. Missing restore on not project, fix exceptions. * Complete project settings detection and restore * Implenent project settings in Windows * Initial utest for Project Settings.
1 parent 2451d71 commit ba10100

File tree

99 files changed

+6527
-2953
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+6527
-2953
lines changed

CHANGELOG.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to http://semver.org/spec/v2.0.0.html[Semantic Versioni
88

99
== https://github.com/robotframework/RIDE[Unreleased]
1010

11+
=== Added
12+
- Added Project Settings concept. The Project Settings is a file named ``ride_settings.cfg`` inside a directory named ``.robot`` located in the Test Suite directory. The search for this directory, is done upwards from the Test Suite directory. You can create an empty directory, ``.robot`` located in the Test Suite directory or any parent directory, and RIDE will create and use the ``ride_settings.cfg``. This way you can have different settings, like: colors, UI language, and Plugins settings. The most relevant example is the creation of different Run Profiles or Arguments, in Test Runner. When you open a Test Suite outside one with Project Settings, you will see a dialog to restart RIDE, to use the ``Global Settings``.
13+
1114
=== Fixed
1215
- Fixed crash when renaming test cases names on Tree (Project Explorer), by cancelling with Escape or by adding a Space in the end.
1316
- Fixed missing text colorization in suites and test settings on Grid Editor.

README.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Likewise, the current version of wxPython, is 4.2.3, but RIDE is known to work w
4444

4545
`pip install -U robotframework-ride`
4646

47-
(3.8 <= python <= 3.14) Install current development version (**2.2dev41**) with:
47+
(3.8 <= python <= 3.14) Install current development version (**2.2dev42**) with:
4848

4949
`pip install -U https://github.com/robotframework/RIDE/archive/develop.zip`
5050

src/robotide/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def main(*args):
6666
print(version.VERSION)
6767
sys.exit(0)
6868
noupdatecheck, debug_console, settings_path, inpath = _parse_args(args)
69+
# print(f"DEBUG: robotide/__init__.py main inpath={inpath}\n")
6970
if len(args) > 3 or '--help' in args:
7071
print(__doc__)
7172
sys.exit()

src/robotide/application/CHANGELOG.html

Lines changed: 19 additions & 17 deletions
Large diffs are not rendered by default.

src/robotide/application/application.py

Lines changed: 153 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@
1515

1616
import builtins
1717
import os
18+
import psutil
1819
import subprocess
20+
import sys
1921

2022
import wx
2123

2224
from contextlib import contextmanager
2325
from pathlib import Path
2426

2527
from ..namespace import Namespace
28+
from ..context import SETTINGS_DIRECTORY
2629
from ..controller import Project
2730
from ..spec import librarydatabase
2831
from ..ui import LoadProgressObserver
@@ -58,6 +61,33 @@
5861
FOREGROUND_TEXT = 'foreground text'
5962
FONT_SIZE = 'font size'
6063
FONT_FACE = 'font face'
64+
SPC = " "
65+
FIVE_SPC = SPC*5
66+
NORMAL_SETTINGS = _('Global Settings')
67+
NORMAL_SETTINGS_DETECTED = _('Global Settings Detected')
68+
PROJECT_SETTINGS = _('Project Settings')
69+
PROJECT_SETTINGS_DETECTED = _('Project Settings Detected')
70+
71+
72+
def restart_RIDE(args:list):
73+
script = os.path.abspath(os.path.dirname(__file__) + '/../__init__.py') # This is probably a different signature
74+
python = sys.executable
75+
arguments = [python, script]
76+
for a in args:
77+
if a:
78+
arguments.append(a)
79+
# print(f"DEBUG: Application.py restart_RIDE arguments={arguments}\n")
80+
"""
81+
try:
82+
process = psutil.Process(os.getpid())
83+
# process.terminate()
84+
for handler in process.open_files() + process.connections():
85+
os.close(handler.fd)
86+
except Exception as e:
87+
pass
88+
"""
89+
str_args = " ".join(arguments)
90+
subprocess.Popen(str_args, shell=True)
6191

6292

6393
class UnthemableWidgetError(Exception):
@@ -84,8 +114,10 @@ def __init__(self, path=None, updatecheck=True, settingspath=None):
84114
self._updatecheck = updatecheck
85115
self.workspace_path = path
86116
self.changed_workspace = False
117+
self.preferences = None
87118
self.settings_path = settingspath
88119
context.APP = self
120+
# print(f"DEBUG: Application.py RIDE __init__ path={self.workspace_path}\n")
89121
wx.App.__init__(self, redirect=False)
90122

91123
def OnInit(self): # Overrides wx method
@@ -94,6 +126,16 @@ def OnInit(self): # Overrides wx method
94126
self._locale = wx.Locale(wx.LANGUAGE_ENGLISH_US) # LANGUAGE_PORTUGUESE
95127
# Needed for SetToolTipString to work
96128
wx.HelpProvider.Set(wx.SimpleHelpProvider()) # DEBUG: adjust to wx versions
129+
# Use Project settings if available
130+
from ..recentfiles import RecentFilesPlugin
131+
self.settings = RideSettings(self.settings_path) # We need this to know the available plugins
132+
self._plugin_loader = PluginLoader(self, self._get_plugin_dirs(), [RecentFilesPlugin],
133+
silent=True)
134+
# print(f"DEBUG: Application.py RIDE OnInit path={self.workspace_path}\n")
135+
self.workspace_path = self.workspace_path or self._get_latest_path()
136+
# print(f"DEBUG: Application.py RIDE OnInit AFTER _get_latest_path() path={self.workspace_path}")
137+
if not self.settings_path:
138+
self.settings_path = self.initialize_project_settings(self.workspace_path)
97139
self.settings = RideSettings(self.settings_path)
98140

99141
class Message:
@@ -105,7 +147,8 @@ class Message:
105147
from ..context import coreplugins, SETTINGS_DIRECTORY
106148
from ..ui.treeplugin import TreePlugin
107149
librarydatabase.initialize_database()
108-
self.preferences = Preferences(self.settings)
150+
# self.preferences = Preferences(self.settings, self.settings_path)
151+
self.reload_preferences(Message)
109152
self.namespace = Namespace(self.settings)
110153
self._controller = Project(self.namespace, self.settings)
111154
# Try to get FontInfo as soon as possible
@@ -115,8 +158,7 @@ class Message:
115158
self.frame = RideFrame(self, self._controller)
116159
# DEBUG self.frame.Show()
117160
self._editor_provider = EditorProvider()
118-
self._plugin_loader = PluginLoader(self, self._get_plugin_dirs(),
119-
coreplugins.get_core_plugins())
161+
self._plugin_loader = PluginLoader(self, self._get_plugin_dirs(), coreplugins.get_core_plugins())
120162
self._plugin_loader.enable_plugins()
121163
perspective = self.settings.get('AUI Perspective', None)
122164
if perspective:
@@ -128,7 +170,7 @@ class Message:
128170
except Exception as e:
129171
print(f"RIDE: There was a problem loading panels position."
130172
f" Please delete the definition 'AUI NB Perspective' in "
131-
f"{os.path.join(SETTINGS_DIRECTORY, 'settings.cfg')}")
173+
f"{self.settings_path or os.path.join(SETTINGS_DIRECTORY, 'settings.cfg')}")
132174
if not isinstance(e, IndexError): # If is with all notebooks disabled, continue
133175
raise e
134176
self.fileexplorerplugin = FileExplorerPlugin(self, self._controller)
@@ -155,15 +197,69 @@ class Message:
155197
RideSettingsChanged(keys=('Excludes', 'init'), old=None, new=None).publish()
156198
PUBLISHER.subscribe(self.change_locale, RideSettingsChanged)
157199
RideSettingsChanged(keys=('General', 'ui language'), old=None, new=None).publish()
200+
PUBLISHER.subscribe(self.reload_preferences, RideSettingsChanged)
158201
wx.CallLater(600, ReleaseNotes(self).bring_to_front)
159202
return True
160203

161204
def OnExit(self):
162205
PUBLISHER.unsubscribe_all()
206+
self.frame.on_exit(None)
163207
self.Destroy()
164208
wx.Exit()
165209
return True
166210

211+
def reload_preferences(self, message):
212+
if message.keys[0] != "General":
213+
return
214+
if self.preferences:
215+
del self.preferences
216+
if self.settings_path:
217+
self.settings = RideSettings(self.settings_path)
218+
self.preferences = Preferences(self.settings, self.settings_path)
219+
try:
220+
if message.keys[1] in ["reload", "restore"]:
221+
# reload plugins (was the original idea)
222+
if message.keys[1] == "reload":
223+
project_or_normal = PROJECT_SETTINGS
224+
project_or_normal_detected = PROJECT_SETTINGS_DETECTED
225+
else:
226+
project_or_normal = NORMAL_SETTINGS
227+
project_or_normal_detected = NORMAL_SETTINGS_DETECTED
228+
229+
from .updatenotifier import _askyesno
230+
if not _askyesno(_("Restart RIDE?"),
231+
f"{FIVE_SPC}{FIVE_SPC}{FIVE_SPC}{FIVE_SPC}{FIVE_SPC}"
232+
f"{project_or_normal_detected}{FIVE_SPC}\n\n"
233+
f"{FIVE_SPC}{_('RIDE must be restarted to fully use these ')}{project_or_normal}"
234+
f"{FIVE_SPC}\n"f"\n\n{FIVE_SPC}{FIVE_SPC}{FIVE_SPC}{FIVE_SPC}{FIVE_SPC}"
235+
f"{_('Click OK to Restart RIDE!')}{FIVE_SPC}{FIVE_SPC}{FIVE_SPC}{FIVE_SPC}{FIVE_SPC}"
236+
f"\n\n", wx.GetActiveWindow(), no_default=False):
237+
return False
238+
args = [f"{'--noupdatecheck' if not self._updatecheck else ''}",
239+
f"--settingspath {message.keys[2]}", f"{message.keys[3]}"]
240+
restart_RIDE(args)
241+
wx.CallLater(1000, self.OnExit)
242+
# The next block was an attempt to reload plugins, in particular Test Runner to
243+
# load the Arguments for the project. This did not work because the plugins are
244+
# loaded at creation of ui/mainframe. For now, we open a new instance of RIDE
245+
# with the test suite/project argument.
246+
"""
247+
# print("DEBUG: application.py RELOAD PLUGINS HERE!")
248+
from time import sleep
249+
for plu in self._plugin_loader.plugins:
250+
# print(f"DEBUG: Application RIDE plugin: {plu} {plu.name}\n"
251+
# f"Plugin object={plu.conn_plugin}")
252+
if plu.name == 'Editor':
253+
plu.conn_plugin._show_editor()
254+
if plu.name == 'Test Runner':
255+
plu.disable()
256+
sleep(2)
257+
plu.enable()
258+
# plu.conn_plugin._show_notebook_tab()
259+
"""
260+
except IndexError:
261+
pass
262+
167263
@staticmethod
168264
def _ApplyThemeToWidget(widget, fore_color=wx.BLUE, back_color=wx.LIGHT_GREY, theme: (None, dict) = None):
169265
if theme is None:
@@ -335,7 +431,6 @@ def _load_data(self):
335431
theme = self.settings.get_without_default('General')
336432
background = theme['background']
337433
foreground = theme['foreground']
338-
# print(f"DEBUG: application.py RIDE _load_data CALL PROGRESS {background=} {foreground=}")
339434
observer = LoadProgressObserver(self.frame, background=background, foreground=foreground)
340435
self._controller.load_data(self.workspace_path, observer)
341436

@@ -363,10 +458,18 @@ def _find_robot_installation():
363458
return None
364459

365460
def _get_latest_path(self):
461+
last_file = None
462+
last_file_path = os.path.join(SETTINGS_DIRECTORY, 'last_file.txt')
463+
if os.path.isfile(last_file_path):
464+
with open(last_file_path, 'r', encoding='utf-8') as fp:
465+
last_file = fp.read()
366466
recent = self._get_recentfiles_plugin()
367467
if not recent or not recent.recent_files:
368-
return None
369-
return recent.recent_files[0]
468+
if last_file:
469+
return last_file
470+
else:
471+
return None
472+
return last_file if last_file and last_file != recent.recent_files[0] else recent.recent_files[0]
370473

371474
def _get_recentfiles_plugin(self):
372475
from ..recentfiles import RecentFilesPlugin
@@ -377,6 +480,49 @@ def _get_recentfiles_plugin(self):
377480
def get_plugins(self):
378481
return self._plugin_loader.plugins
379482

483+
def initialize_project_settings(self, path):
484+
""" Detects if exists a directory named .robot at project root, and creates ride_settings.cfg
485+
if not exists, or returns the path to existing file.
486+
"""
487+
from ..preferences import initialize_settings
488+
default_dir = path if os.path.isdir(path) else os.path.dirname(path)
489+
local_settings_dir = os.path.join(default_dir, '.robot')
490+
old_settings_dir = self.settings_path
491+
# print(f"DEBUG: Project.py Project initialize_project_settings ENTER: path={local_settings_dir}")
492+
if os.path.isdir(local_settings_dir):
493+
# old_settings = self.internal_settings.get_without_default('General')
494+
# old_bkg = old_settings['background']
495+
local_settings = os.path.join(local_settings_dir, 'ride_settings.cfg')
496+
self.settings_path = local_settings
497+
if os.path.isfile(local_settings):
498+
# os.putenv('RIDESETTINGS', local_settings)
499+
os.environ['RIDESETTINGS'] = local_settings
500+
self.settings = RideSettings(local_settings)
501+
# print(f"DEBUG: Project.py Project initialize_project_settings EXISTING project settings "
502+
# f"{local_settings=} \nRIDESETTINGS={os.environ['RIDESETTINGS']}"
503+
# f"\nsettings={self.dump_settings()}")
504+
else:
505+
default = RideSettings()
506+
settings_path = default.user_path
507+
new_path = initialize_settings(path=settings_path, dest_file_name=local_settings)
508+
# print(f"DEBUG: Project.py Project initialize_project_settings NEW project settings new_path={new_path}"
509+
# f" local_settings={local_settings}")
510+
self.settings = RideSettings(new_path)
511+
# new_settings = self.settings.get_without_default('General')
512+
# new_bkg = new_settings.get_without_default('background')
513+
# RideSettingsChanged(keys=('General', 'background'), old=old_bkg, new=new_bkg).publish()
514+
else:
515+
os.environ['RIDESETTINGS'] = ''
516+
# print(f"DEBUG: Project.py Project initialize_project_settings RETURNING: path={self.settings_path}")
517+
if self.settings_path != old_settings_dir:
518+
RideSettingsChanged(keys=('General', ), old=None, new=None).publish()
519+
return self.settings_path
520+
521+
def dump_settings(self):
522+
keys = [items for items in self.settings]
523+
values = [f"{val}={self.settings[val]}" for val in keys]
524+
return values
525+
380526
def register_preference_panel(self, panel_class):
381527
"""Add the given panel class to the list of known preference panels"""
382528
self.preferences.add(panel_class)

src/robotide/application/pluginconnector.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
from ..context import LOG
1818

1919

20-
def plugin_factory(application, plugin_class):
20+
def plugin_factory(application, plugin_class, silent=False):
2121
try:
2222
plugin = plugin_class(application)
2323
except Exception as e:
2424
print(e)
2525
msg, traceback = utils.get_error_details()
26-
return BrokenPlugin(msg, traceback, plugin_class)
26+
if not silent:
27+
return BrokenPlugin(msg, traceback, plugin_class)
2728
else:
2829
return PluginConnector(plugin, application)
2930

src/robotide/application/pluginloader.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525

2626
class PluginLoader(object):
2727

28-
def __init__(self, application, load_dirs, standard_classes):
28+
def __init__(self, application, load_dirs, standard_classes, silent=False):
2929
self._load_errors = []
30-
self.plugins = [plugin_factory(application, cls) for cls in standard_classes + self._find_classes(load_dirs)]
30+
self.silent = silent
31+
self.plugins = [plugin_factory(application, cls, silent) for cls in standard_classes +
32+
self._find_classes(load_dirs)]
3133
if self._load_errors:
3234
LOG.error('\n\n'.join(self._load_errors))
3335

@@ -74,8 +76,8 @@ def _import_classes(self, path):
7476
m_module = importlib.util.module_from_spec(spec)
7577
spec.loader.exec_module(m_module)
7678
except Exception as err:
77-
self._load_errors.append("Importing plugin module '%s' failed:\n%s"
78-
% (path, err))
79+
if not self.silent:
80+
self._load_errors.append(f"Importing plugin module '{path}' failed:\n{err}")
7981
return []
8082
return [cls for _, cls in
8183
inspect.getmembers(m_module, predicate=inspect.isclass)]

src/robotide/application/releasenotes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ def set_content(self, html_win, content):
175175
</ul>
176176
<p><strong>New Features and Fixes Highlights</strong></p>
177177
<ul class="simple">
178+
<li>Added Project Settings concept. The Project Settings is a file named <b>ride_settings.cfg</b> inside a directory
179+
named <b>.robot</b> located in the Test Suite directory.</li>
178180
<li>Fixed crash when renaming test cases names on Tree (Project Explorer), by cancelling with Escape or by adding a Space
179181
in the end.</li>
180182
<li>Fixed missing text colorization in suites and test settings on Grid Editor.</li>
@@ -243,7 +245,7 @@ def set_content(self, html_win, content):
243245
<pre class="literal-block">python -m robotide.postinstall -install</pre>
244246
<p>or</p>
245247
<pre class="literal-block">ride_postinstall.py -install</pre>
246-
<p>RIDE {VERSION} was released on 17/August/2025.</p>
248+
<p>RIDE {VERSION} was released on 16/September/2025.</p>
247249
<!-- <br/>
248250
<h3>May The Fourth Be With You!</h3>
249251
<h3>Celebrate the bank holiday, 10th June, Day of Portugal, Portuguese Communities and Camões!!</h3>

src/robotide/contrib/testrunner/testrunnerplugin.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ def _subscribe_to_events(self):
276276

277277
def on_settings_changed(self, message):
278278
"""Updates settings"""
279+
if message.keys[0] == "General":
280+
return
279281
section, setting = message.keys
280282
# print("DEBUG: enter OnSettingsChanged section %s" % (section))
281283
if section == 'Test Run': # DEBUG temporarily we have two sections
@@ -1383,6 +1385,8 @@ def __init__(self, editor, settings):
13831385

13841386
def on_settings_changed(self, message):
13851387
"""Redraw colors and font if settings are modified"""
1388+
if message.keys[0] == "General":
1389+
return
13861390
section, _ = message.keys
13871391
if section == 'Test Runner':
13881392
self._set_styles()

0 commit comments

Comments
 (0)