Skip to content

Commit 2f06185

Browse files
committed
Store recent files in QSettings
1 parent cbf5b10 commit 2f06185

3 files changed

Lines changed: 491 additions & 43 deletions

File tree

i18n/si/msgs.jaml

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,9 @@ settings.py:
302302
values=: false
303303
): false
304304
version.py:
305-
4.25.0: false
306-
4.25.0.dev0+8cea3f1: false
307-
8cea3f1fed66a72e7fef95c836a81b33d3eb295e: false
305+
4.28.0: false
306+
4.28.0.dev0+0a27ec5: false
307+
0a27ec5c8efe4e6fdc3f3e057b60df46491cad29: false
308308
.dev: false
309309
widget.py:
310310
OWBaseWidget: false
@@ -921,8 +921,6 @@ utils/filedialogs.py:
921921
Change extension to {suggested_ext}: Spremeni končnico v {suggested_ext}
922922
'Save as ': 'Shrani kot '
923923
Back: Nazaj
924-
def `unambiguous_paths`:
925-
{sep}|{re.escape(os.path.altsep)}: false
926924
def `format_filter`:
927925
{} (*{}): false
928926
' *': false
@@ -945,14 +943,37 @@ utils/filedialogs.py:
945943
'{0.__class__.__name__}(abspath={0.abspath!r}, ': false
946944
'prefix={0.prefix!r}, relpath={0.relpath!r}, ': false
947945
title={0.title!r}): false
948-
class `RecentPathsWidgetMixin`:
949-
def `_check_init`:
950-
RecentPathsWidgetMixin.__init__ was not called: false
946+
def `_check_init`:
947+
def `wrapper`:
948+
{type(self).__name__}.__init__ was not called: false
949+
class `_RecentPathsWidgetMixinBase`:
951950
def `_search_paths`:
952951
basedir: false
953-
class `RecentPathsWComboMixin`:
954-
def `set_file_list`:
955-
(none): (prazno)
952+
class `RecentPathsWidgetMixin`:
953+
def `__init__`:
954+
"RecentPathsWidgetMixin is deprecated because it exposes user's ": false
955+
paths; use LocalRecentPathsWidgetMixin instead.: false
956+
class `LocalRecentPathsWidgetMixin`:
957+
def `__init__`:
958+
active_path: false
959+
'LocalRecentPathsWidgetMixin must be mixed into a widget that defines ': false
960+
'active_path setting; use LocalRecentPathsWComboMixin if you want a ': false
961+
combo box with active path handling: false
962+
def `__setting_name`:
963+
^OW: false
964+
[^A-Za-z_.]: false
965+
([a-z])([A-Z]): false
966+
{m.group(1)}-{m.group(2)}: false
967+
recent-paths/{name}: false
968+
def `_ensure_recent_paths`:
969+
recent_paths: false
970+
def `store_recent_paths`:
971+
recent_paths: false
972+
def `_relocate_recent_files`:
973+
recent_paths: false
974+
class `_RecentPathsWComboMixinBase`:
975+
def `_set_file_list`:
976+
(none): false
956977
def `update_file_list`:
957978
basedir: false
958979
utils/itemdelegates.py:

orangewidget/utils/filedialogs.py

Lines changed: 183 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import functools
12
import pathlib
3+
import re
24
from collections import Counter
35
from itertools import count
6+
import warnings
47

58
import os
69
import sys
710
import typing
811
from typing import Tuple
912

10-
from AnyQt.QtCore import QFileInfo, Qt
13+
from AnyQt.QtCore import QFileInfo, Qt, QSettings
1114
from AnyQt.QtGui import QBrush
1215
from AnyQt.QtWidgets import \
1316
QMessageBox, QFileDialog, QFileIconProvider, QComboBox
@@ -230,6 +233,14 @@ def __eq__(self, other):
230233
self.prefix == other.prefix and
231234
self.relpath == other.relpath))
232235

236+
def to_list(self):
237+
return [self.abspath, self.prefix, self.relpath,
238+
self.title, self.sheet, self.file_format]
239+
240+
@classmethod
241+
def from_list(cls, lst):
242+
return cls(*lst)
243+
233244
@staticmethod
234245
def create(path, searchpaths, **kwargs):
235246
"""
@@ -330,7 +341,16 @@ def __repr__(self):
330341
__str__ = __repr__
331342

332343

333-
class RecentPathsWidgetMixin:
344+
def _check_init(f):
345+
@functools.wraps(f)
346+
def wrapper(self, *args, **kwargs):
347+
if not self._init_called:
348+
raise RuntimeError(f"{type(self).__name__}.__init__ was not called")
349+
return f(self, *args, **kwargs)
350+
return wrapper
351+
352+
353+
class _RecentPathsWidgetMixinBase:
334354
"""
335355
Provide a setting with recent paths and relocation capabilities
336356
@@ -345,8 +365,8 @@ class RecentPathsWidgetMixin:
345365
and overload the method `select_file`, for instance like this
346366
347367
def select_file(self, n):
348-
super().select_file(n)
349-
self.open_file()
368+
recent = super().select_file(n)
369+
self.open_file(recent)
350370
351371
The mixin works by adding a `recent_path` setting storing a list of
352372
instances of :obj:`RecentPath` (not pure strings). The widget can also
@@ -362,67 +382,144 @@ def select_file(self, n):
362382

363383
#: list with search paths; overload to add, say, documentation datasets dir
364384
SEARCH_PATHS = []
385+
MAX_RECENT_PATHS = 15
365386

366-
#: List[RecentPath]
367-
recent_paths = Setting([])
368-
387+
# This is just a declaration. The definition is provided by subclasses.
388+
recent_paths: list[RecentPath]
369389
_init_called = False
370390

371391
def __init__(self):
372-
super().__init__()
373392
self._init_called = True
374-
self._relocate_recent_files()
375-
376-
def _check_init(self):
377-
if not self._init_called:
378-
raise RuntimeError("RecentPathsWidgetMixin.__init__ was not called")
393+
super().__init__()
379394

380395
def _search_paths(self):
381396
basedir = self.workflowEnv().get("basedir", None)
382397
if basedir is None:
383398
return self.SEARCH_PATHS
384399
return self.SEARCH_PATHS + [("basedir", basedir)]
385400

386-
def _relocate_recent_files(self):
387-
self._check_init()
401+
@_check_init
402+
def _relocated_recent_files(self, paths):
388403
search_paths = self._search_paths()
389404
rec = []
390-
for recent in self.recent_paths:
405+
for recent in paths:
391406
kwargs = dict(title=recent.title, sheet=recent.sheet, file_format=recent.file_format)
392407
resolved = recent.resolve(search_paths)
393408
if resolved is not None:
394409
rec.append(
395410
RecentPath.create(resolved.abspath, search_paths, **kwargs))
396-
elif recent.search(search_paths) is not None:
397-
rec.append(
398-
RecentPath.create(recent.search(search_paths), search_paths, **kwargs)
411+
elif (path := recent.search(search_paths)) is not None:
412+
rec.append(RecentPath.create(path, search_paths, **kwargs)
399413
)
400414
else:
401415
rec.append(recent)
402-
# change the list in-place for the case the widgets wraps this list
403-
# in some model (untested!)
404-
self.recent_paths[:] = rec
416+
return rec
405417

418+
@_check_init
406419
def add_path(self, filename):
407420
"""Add (or move) a file name to the top of recent paths"""
408-
self._check_init()
409421
recent = RecentPath.create(filename, self._search_paths())
410422
if recent in self.recent_paths:
411423
self.recent_paths.remove(recent)
412424
self.recent_paths.insert(0, recent)
425+
del self.recent_paths[self.MAX_RECENT_PATHS:]
426+
return recent
413427

428+
@_check_init
414429
def select_file(self, n):
415430
"""Move the n-th file to the top of the list"""
416431
recent = self.recent_paths[n]
417432
del self.recent_paths[n]
418433
self.recent_paths.insert(0, recent)
434+
return recent
435+
436+
437+
class RecentPathsWidgetMixin(_RecentPathsWidgetMixinBase):
438+
recent_paths: list[RecentPath] = Setting([])
439+
440+
def __init__(self):
441+
super().__init__()
442+
warnings.warn(
443+
"RecentPathsWidgetMixin is deprecated because it exposes user's "
444+
"paths; use LocalRecentPathsWidgetMixin instead.",
445+
DeprecationWarning, stacklevel=2)
446+
self._relocate_recent_files()
447+
448+
def _relocate_recent_files(self):
449+
# change the list in-place for the case the widgets wraps this list
450+
# in some model (untested!)
451+
self.recent_paths[:] = self._relocated_recent_files(self.recent_paths)
419452

420453
def last_path(self):
421-
"""Return the most recent absolute path or `None` if there is none"""
422454
return self.recent_paths[0].abspath if self.recent_paths else None
423455

424456

425-
class RecentPathsWComboMixin(RecentPathsWidgetMixin):
457+
class LocalRecentPathsWidgetMixin(_RecentPathsWidgetMixinBase):
458+
active_path: typing.Optional[RecentPath]
459+
DefaultRecentPaths: list[RecentPath] = []
460+
461+
def __init__(self):
462+
super().__init__()
463+
assert hasattr(self, "active_path"), (
464+
"LocalRecentPathsWidgetMixin must be mixed into a widget that defines "
465+
"active_path setting; use LocalRecentPathsWComboMixin if you want a "
466+
"combo box with active path handling")
467+
self._ensure_recent_paths()
468+
self._relocate_recent_files()
469+
self.update_active_path()
470+
471+
@classmethod
472+
def __setting_name(cls):
473+
name = cls.__qualname__
474+
name = re.sub("^OW", "", name)
475+
name = re.sub("[^A-Za-z_.]", "", name)
476+
name = re.sub("([a-z])([A-Z])", lambda m: f"{m.group(1)}-{m.group(2)}", name)
477+
name = name.lower()
478+
name = f"recent-paths/{name}"
479+
return name
480+
481+
@classmethod
482+
def _ensure_recent_paths(cls):
483+
if "recent_paths" in cls.__dict__:
484+
return
485+
486+
paths = QSettings().value(cls.__setting_name(), [], type=list)
487+
paths = [RecentPath.from_list(setting) for setting in paths]
488+
if not paths:
489+
# Use a copy to avoid sharing the mutable DefaultRecentPaths list
490+
paths = cls.DefaultRecentPaths[:]
491+
setattr(cls, "recent_paths", paths)
492+
493+
@classmethod
494+
def store_recent_paths(cls):
495+
QSettings().setValue(
496+
cls.__setting_name(),
497+
[path.to_list() for path in getattr(cls, "recent_paths")]
498+
)
499+
500+
def _relocate_recent_files(self):
501+
cls = type(self)
502+
paths = getattr(cls, "recent_paths", [])
503+
paths = self._relocated_recent_files(paths)
504+
setattr(cls, "recent_paths", paths)
505+
cls.store_recent_paths()
506+
507+
def add_path(self, filename):
508+
recent = super().add_path(filename)
509+
self.store_recent_paths()
510+
return recent
511+
512+
def select_file(self, n):
513+
recent = super().select_file(n)
514+
self.store_recent_paths()
515+
return recent
516+
517+
def update_active_path(self):
518+
if self.active_path:
519+
self.active_path = self._relocated_recent_files([self.active_path])[0]
520+
521+
522+
class _RecentPathsWComboMixinBase:
426523
"""
427524
Adds file combo handling to :obj:`RecentPathsWidgetMixin`.
428525
@@ -438,26 +535,28 @@ def __init__(self):
438535

439536
def add_path(self, filename):
440537
"""Add (or move) a file name to the top of recent paths"""
441-
super().add_path(filename)
538+
recent = super().add_path(filename)
442539
self.set_file_list()
540+
return recent
443541

444542
def select_file(self, n):
445543
"""Move the n-th file to the top of the list"""
446-
super().select_file(n)
544+
recent = super().select_file(n)
447545
self.set_file_list()
546+
return recent
448547

449-
def set_file_list(self):
548+
@_check_init
549+
def _set_file_list(self, n):
450550
"""
451551
Sets the items in the file list combo
452552
"""
453-
self._check_init()
454553
self.file_combo.clear()
455554
if not self.recent_paths:
456555
self.file_combo.addItem("(none)")
457556
self.file_combo.model().item(0).setEnabled(False)
458557
self.file_combo.setToolTip("")
459558
else:
460-
self.file_combo.setToolTip(self.recent_paths[0].abspath)
559+
assert 0 <= n < len(self.recent_paths)
461560
paths = unambiguous_paths(
462561
[recent.abspath for recent in self.recent_paths], minlevel=2)
463562
for i, recent, path in zip(count(), self.recent_paths, paths):
@@ -466,8 +565,62 @@ def set_file_list(self):
466565
if not os.path.exists(recent.abspath):
467566
self.file_combo.setItemData(i, QBrush(Qt.red),
468567
Qt.ForegroundRole)
568+
self.file_combo.setToolTip(self.recent_paths[n].abspath)
569+
self.file_combo.setCurrentIndex(n)
469570

470571
def update_file_list(self, key, value, oldvalue):
471572
if key == "basedir":
472573
self._relocate_recent_files()
473574
self.set_file_list()
575+
576+
577+
class RecentPathsWComboMixin(RecentPathsWidgetMixin,
578+
_RecentPathsWComboMixinBase):
579+
def set_file_list(self):
580+
self._set_file_list(0)
581+
582+
583+
class LocalRecentPathsWComboMixin(LocalRecentPathsWidgetMixin,
584+
_RecentPathsWComboMixinBase):
585+
active_path: typing.Optional[RecentPath] = Setting(None)
586+
587+
def __init__(self):
588+
super().__init__()
589+
if self.active_path is None:
590+
if self.recent_paths:
591+
self.active_path = self.recent_paths[0]
592+
else:
593+
if self.active_path not in self.recent_paths:
594+
self.recent_paths.insert(0, self.active_path)
595+
self.store_recent_paths()
596+
597+
@_check_init
598+
def set_file_list(self):
599+
if self.active_path in self.recent_paths:
600+
n = self.recent_paths.index(self.active_path)
601+
else:
602+
n = 0
603+
self._set_file_list(n)
604+
605+
def add_path(self, filename):
606+
recent = super().add_path(filename)
607+
self.active_path = recent
608+
return recent
609+
610+
def select_file(self, n):
611+
recent = super().select_file(n)
612+
self.active_path = recent
613+
return recent
614+
615+
@classmethod
616+
def merge_paths(cls, old_paths: list[RecentPath]):
617+
cls._ensure_recent_paths()
618+
for old_path in old_paths:
619+
if old_path in cls.recent_paths:
620+
cls.recent_paths.remove(old_path)
621+
cls.recent_paths[:0] = old_paths
622+
del cls.recent_paths[cls.MAX_RECENT_PATHS:]
623+
cls.store_recent_paths()
624+
625+
def last_path(self):
626+
return self.active_path and self.active_path.abspath

0 commit comments

Comments
 (0)