1
1
import os .path
2
+ import sys
3
+ import re
2
4
3
- from AnyQt .QtWidgets import QFileDialog , QGridLayout , QWidget
5
+ from AnyQt .QtWidgets import QFileDialog , QGridLayout , QMessageBox
4
6
5
7
from Orange .data .table import Table
6
8
from Orange .data .io import TabReader , CSVReader , PickleReader , ExcelReader
9
11
from Orange .widgets .utils .widgetpreview import WidgetPreview
10
12
from Orange .widgets .widget import Input
11
13
12
- class FileDialog (QFileDialog ):
13
- def changeEvent (self , e ):
14
- print (e )
15
- super ().selectFile (e )
16
14
17
15
class OWSave (widget .OWWidget ):
18
16
name = "Save Data"
@@ -24,170 +22,122 @@ class OWSave(widget.OWWidget):
24
22
settings_version = 2
25
23
26
24
writers = [TabReader , CSVReader , PickleReader , ExcelReader ]
27
- filters = [f"{ w .DESCRIPTION } (*.*)" for w in writers ]
28
- filt_ext = {filter : w .EXTENSIONS [0 ] for filter , w in zip (filters , writers )}
25
+ filters = {
26
+ ** {f"{ w .DESCRIPTION } (*{ w .EXTENSIONS [0 ]} )" : w
27
+ for w in writers },
28
+ ** {f"Compressed { w .DESCRIPTION } (*{ w .EXTENSIONS [0 ]} .gz)" : w
29
+ for w in writers }
30
+ }
29
31
userhome = os .path .expanduser (f"~{ os .sep } " )
30
32
31
33
class Inputs :
32
34
data = Input ("Data" , Table )
33
35
34
36
class Error (widget .OWWidget .Error ):
35
- unsupported_sparse = widget .Msg ("Use .pkl format for sparse data." )
37
+ unsupported_sparse = widget .Msg ("Use Pickle format for sparse data." )
36
38
no_file_name = widget .Msg ("File name is not set." )
37
39
general_error = widget .Msg ("{}" )
38
40
39
41
class Warning (widget .OWWidget .Warning ):
40
- ignored_flag = widget .Msg ("{} ignored for this format." )
42
+ type_annotation_ignored = widget .Msg (
43
+ "Type annotation setting is ignored for this format." )
41
44
42
45
want_main_area = False
43
46
resizing_enabled = False
44
47
45
- compress : bool
46
- add_type_annotations : bool
47
-
48
48
last_dir = Setting ("" )
49
- filter = Setting (filters [ 0 ] )
50
- compress = Setting (False )
49
+ filter = Setting (next ( iter ( filters )) )
50
+ filename = Setting ("" , schema_only = True )
51
51
add_type_annotations = Setting (True )
52
52
auto_save = Setting (False )
53
53
54
54
def __init__ (self ):
55
55
super ().__init__ ()
56
56
self .data = None
57
- self .filename = ""
58
- self .writer = self .writers [0 ]
59
57
60
58
grid = QGridLayout ()
61
- gui .widgetBox (self .controlArea , box = True , orientation = grid )
62
- grid .setSpacing (8 )
63
- self .bt_save = gui .button (None , self , "Save" , callback = self .save_file )
64
- grid .addWidget (self .bt_save , 0 , 0 )
65
- grid .addWidget (
66
- gui .button (None , self , "Save as ..." , callback = self .save_file_as ),
67
- 0 , 1 )
68
- grid .addWidget (
69
- gui .checkBox (None , self , "auto_save" ,
70
- "Autosave when receiving new data" ,
71
- callback = self ._update_controls ),
72
- 1 , 0 , 1 , 2 )
73
- grid .addWidget (QWidget (), 2 , 0 , 1 , 2 )
74
-
59
+ gui .widgetBox (self .controlArea , orientation = grid )
75
60
grid .addWidget (
76
61
gui .checkBox (
77
62
None , self , "add_type_annotations" ,
78
- "Save with type annotations" , callback = self ._update_controls ),
79
- 3 , 0 , 1 , 2 )
63
+ "Save with type annotations" , callback = self ._update_messages ),
64
+ 0 , 0 , 1 , 2 )
65
+ grid .setRowMinimumHeight (1 , 8 )
80
66
grid .addWidget (
81
67
gui .checkBox (
82
- None , self , "compress" , "Compress file (gzip)" ,
83
- callback = self ._update_controls ),
84
- 4 , 0 , 1 , 2 )
68
+ None , self , "auto_save" , "Autosave when receiving new data" ,
69
+ callback = self ._update_messages ),
70
+ 2 , 0 , 1 , 2 )
71
+ grid .setRowMinimumHeight (3 , 8 )
72
+ self .bt_save = gui .button (None , self , "Save" , callback = self .save_file )
73
+ grid .addWidget (self .bt_save , 4 , 0 )
74
+ grid .addWidget (
75
+ gui .button (None , self , "Save as ..." , callback = self .save_file_as ),
76
+ 4 , 1 )
85
77
86
78
self .adjustSize ()
87
- self ._update_controls ()
79
+ self ._update_messages ()
80
+
81
+ @property
82
+ def writer (self ):
83
+ return self .filters [self .filter ]
88
84
89
85
@Inputs .data
90
86
def dataset (self , data ):
91
87
self .Error .clear ()
92
88
self .data = data
93
-
94
- self ._update_controls ()
95
- if self .data is None :
96
- self .info .set_input_summary (self .info .NoInput )
97
- else :
98
- self .info .set_input_summary (
99
- str (len (self .data )),
100
- f"Data set { self .data .name or '(no name)' } "
101
- f"with { len (self .data )} instances" )
102
-
89
+ self ._update_status ()
90
+ self ._update_messages ()
103
91
if self .auto_save and self .filename :
104
92
self .save_file ()
105
93
106
- def save_file_as (self ):
107
- if self .filename :
108
- start_dir = self .filename
109
- else :
110
- data_name = getattr (self .data , 'name' , '' )
111
- if data_name :
112
- data_name += self .filt_ext [self .filter ]
113
- start_dir = os .path .join (self .last_dir or self .userhome , data_name )
114
-
115
- dlg = FileDialog (None , "Set File" , start_dir , ";;" .join (self .filters ))
116
- dlg .setLabelText (dlg .Accept , "Select" )
117
- dlg .setAcceptMode (dlg .AcceptSave )
118
- dlg .setSupportedSchemes (["file" ])
119
- dlg .selectNameFilter (self .filter )
120
- dlg .setOption (QFileDialog .HideNameFilterDetails )
121
- dlg .currentChanged .connect (print )
122
- if dlg .exec () == dlg .Rejected :
123
- return
124
-
125
- filename = dlg .selectedFiles ()[0 ]
126
- selected_filter = dlg .selectedNameFilter ()
127
-
128
- # filename, selected_filter = QFileDialog.getSaveFileName(
129
- # self, "Save data", start_dir, ";;".join(self.filters), self.filter,
130
- # QFileDialog.HideNameFilterDetails)
131
- if not filename :
132
- return
133
-
134
- self .filename = filename
135
- self .last_dir = os .path .split (filename )[0 ]
136
- self .filter = selected_filter
137
- self .writer = self .writers [self .filters .index (self .filter )]
138
- self ._update_controls ()
139
- self .save_file ()
140
-
141
94
def save_file (self ):
142
95
if not self .filename :
143
96
self .save_file_as ()
144
97
return
98
+
145
99
self .Error .general_error .clear ()
146
- if not self ._can_save ():
100
+ if self .data is None \
101
+ or not self .filename \
102
+ or (self .data .is_sparse ()
103
+ and not self .writer .SUPPORT_SPARSE_DATA ):
147
104
return
148
105
try :
149
106
self .writer .write (
150
- self ._fullname () , self .data , self .add_type_annotations )
107
+ self .filename , self .data , self .add_type_annotations )
151
108
except IOError as err_value :
152
109
self .Error .general_error (str (err_value ))
153
110
154
- def _fullname (self ):
155
- return self .filename \
156
- + ".gz" * self .writer .SUPPORT_COMPRESSED * self .compress
157
-
158
- def _update_controls (self ):
159
- if self .filename :
160
- self .bt_save .setText (
161
- f"Save as { os .path .split (self ._fullname ())[1 ]} " )
162
- else :
163
- self .bt_save .setText ("Save" )
164
- self .Error .no_file_name (shown = not self .filename and self .auto_save )
111
+ def save_file_as (self ):
112
+ filename , selected_filter = self .get_save_filename ()
113
+ if not filename :
114
+ return
115
+ self .filename = filename
116
+ self .filter = selected_filter
117
+ self .last_dir = os .path .split (self .filename )[0 ]
118
+ self .bt_save .setText (f"Save as { os .path .split (filename )[1 ]} " )
119
+ self ._update_messages ()
120
+ self .save_file ()
165
121
122
+ def _update_messages (self ):
123
+ self .Error .no_file_name (
124
+ shown = not self .filename and self .auto_save )
166
125
self .Error .unsupported_sparse (
167
126
shown = self .data is not None and self .data .is_sparse ()
168
127
and self .filename and not self .writer .SUPPORT_SPARSE_DATA )
128
+ self .Warning .type_annotation_ignored (
129
+ shown = self .data is not None and self .filename
130
+ and self .add_type_annotations
131
+ and not self .writer .OPTIONAL_TYPE_ANNOTATIONS )
169
132
170
- if self .data is None or not self .filename :
171
- self .Warning .ignored_flag .clear ()
133
+ def _update_status (self ):
134
+ if self .data is None :
135
+ self .info .set_input_summary (self .info .NoInput )
172
136
else :
173
- no_compress = self .compress \
174
- and not self .writer .SUPPORT_COMPRESSED
175
- no_anotation = self .add_type_annotations \
176
- and not self .writer .OPTIONAL_TYPE_ANNOTATIONS
177
- ignored = [
178
- "" ,
179
- "Compression flag is" ,
180
- "Type annotation flag is" ,
181
- "Compression and type annotation flags are"
182
- ][no_compress + 2 * no_anotation ]
183
- self .Warning .ignored_flag (ignored , shown = bool (ignored ))
184
-
185
- def _can_save (self ):
186
- return not (
187
- self .data is None
188
- or not self .filename
189
- or self .data .is_sparse () and not self .writer .SUPPORT_SPARSE_DATA
190
- )
137
+ self .info .set_input_summary (
138
+ str (len (self .data )),
139
+ f"Data set { self .data .name or '(no name)' } "
140
+ f"with { len (self .data )} instances" )
191
141
192
142
def send_report (self ):
193
143
self .report_data_brief (self .data )
@@ -196,17 +146,126 @@ def send_report(self):
196
146
self .report_items ((
197
147
("File name" , self .filename or "not set" ),
198
148
("Format" , writer .DESCRIPTION ),
199
- ("Compression" , writer .SUPPORT_COMPRESSED and noyes [self .compress ]),
200
149
("Type annotations" ,
201
150
writer .OPTIONAL_TYPE_ANNOTATIONS
202
151
and noyes [self .add_type_annotations ])
203
152
))
204
153
205
154
@classmethod
206
155
def migrate_settings (cls , settings , version = 0 ):
207
- settings .filter = next (iter (cls .filt_ext ))
208
- # if version < 2:
209
- # settings["filter"] = settings.pop("filetype")
156
+ if version < 2 :
157
+ prev_filter = settings .pop ("filetype" , None )
158
+ if prev_filter is None :
159
+ settings ["filter" ] = next (iter (cls .filters ))
160
+ else :
161
+ prev_ext = cls ._extension_from_filter (prev_filter )
162
+ prev_filter = prev_filter .split ("(" )[0 ]
163
+ if settings .pop ("compress" , False ):
164
+ prev_filter = f"Compressed { prev_filter } (*{ prev_ext } .gz)"
165
+ else :
166
+ prev_filter = f"{ prev_filter } (*{ prev_ext } )"
167
+ settings ["filter" ] = prev_filter
168
+
169
+ def _initial_start_dir (self ):
170
+ if self .filename and os .path .exists (os .path .split (self .filename )[0 ]):
171
+ return self .filename
172
+ else :
173
+ data_name = getattr (self .data , 'name' , '' )
174
+ if data_name :
175
+ data_name += self .writer .EXTENSIONS [0 ]
176
+ return os .path .join (self .last_dir or self .userhome , data_name )
177
+
178
+ @staticmethod
179
+ def _replace_extension (filename , extension ):
180
+ if filename .endswith (extension ): # it may contain dots before extension
181
+ return filename
182
+ last_fn = None
183
+ while last_fn != filename :
184
+ last_fn , filename = filename , os .path .splitext (filename )[0 ]
185
+ return filename + extension
186
+
187
+ @staticmethod
188
+ def _extension_from_filter (selected_filter ):
189
+ return re .search (r".*\(\*?(\..*)\)$" , selected_filter ).group (1 )
190
+
191
+ # As of Qt 5.9, QFileDialog.setDefaultSuffix does not support double
192
+ # suffixes, not even in non-native dialogs. We handle each OS separately.
193
+ if sys .platform == "darwin" :
194
+ # On macOS, is double suffixes are passed to the dialog, they are
195
+ # appended multiple times even if already present (QTBUG-44227).
196
+ # The only known workaround with native dialog is to use suffix *.*.
197
+ # We add the correct suffix after closing the dialog and only then check
198
+ # if the file exists and ask whether to override.
199
+ # It is a bit confusing that the user does not see the final name in the
200
+ # dialog, but I see no better solution.
201
+ def get_save_filename (self ):
202
+ def no_suffix (filt ):
203
+ return filt .split ("(" )[0 ] + "(*.*)"
204
+
205
+ mac_filters = {no_suffix (f ): f for f in self .filters }
206
+ filename = self ._initial_start_dir ()
207
+ while True :
208
+ dlg = QFileDialog (
209
+ None , "Save File" , filename , ";;" .join (mac_filters ))
210
+ dlg .setAcceptMode (dlg .AcceptSave )
211
+ dlg .selectNameFilter (no_suffix (self .filter ))
212
+ dlg .setOption (QFileDialog .HideNameFilterDetails )
213
+ dlg .setOption (QFileDialog .DontConfirmOverwrite )
214
+ if dlg .exec () == dlg .Rejected :
215
+ return "" , ""
216
+ filename = dlg .selectedFiles ()[0 ]
217
+ selected_filter = mac_filters [dlg .selectedNameFilter ()]
218
+ filename = self ._replace_extension (
219
+ filename , self ._extension_from_filter (selected_filter ))
220
+ if not os .path .exists (filename ) or QMessageBox .question (
221
+ self , "Overwrite file?" ,
222
+ f"File { os .path .split (filename )[1 ]} already exists.\n "
223
+ "Overwrite?" ) == QMessageBox .Yes :
224
+ return filename , selected_filter
225
+
226
+ elif sys .platform == "win32" :
227
+ # TODO: This is not tested!!!
228
+ # Windows native dialog may work correctly; if not, we may do the same
229
+ # as for macOS?
230
+ def get_save_filename (self ):
231
+ return QFileDialog .getSaveFileName (
232
+ self , "Save File" , self ._initial_start_dir (),
233
+ ";;" .join (self .filters ), self .filter )
234
+
235
+ else : # Linux and any unknown platforms
236
+ # Qt does not use a native dialog on Linux, so we can connect to
237
+ # filterSelected and to overload selectFile to change the extension
238
+ # while the dialog is open.
239
+ # For unknown platforms (which?), we also use the non-native dialog to
240
+ # be sure we know what happens.
241
+ class SaveFileDialog (QFileDialog ):
242
+ # pylint: disable=protected-access
243
+ def __init__ (self , * args , ** kwargs ):
244
+ super ().__init__ (* args , ** kwargs )
245
+ self .setAcceptMode (QFileDialog .AcceptSave )
246
+ self .setOption (QFileDialog .DontUseNativeDialog )
247
+ self .filterSelected .connect (self .updateDefaultExtension )
248
+
249
+ def updateDefaultExtension (self , selected_filter ):
250
+ self .suffix = OWSave ._extension_from_filter (selected_filter )
251
+ files = self .selectedFiles ()
252
+ if files and not os .path .isdir (files [0 ]):
253
+ self .selectFile (files [0 ].split ("." )[0 ])
254
+
255
+ def selectFile (self , filename ):
256
+ filename = OWSave ._replace_extension (filename , self .suffix )
257
+ super ().selectFile (filename )
258
+
259
+ def get_save_filename (self ):
260
+ dlg = self .SaveFileDialog (
261
+ None , "Save File" , self ._initial_start_dir (),
262
+ ";;" .join (self .filters ))
263
+ dlg .selectNameFilter (self .filter )
264
+ dlg .updateDefaultExtension (self .filter )
265
+ if dlg .exec () == QFileDialog .Rejected :
266
+ return "" , ""
267
+ else :
268
+ return dlg .selectedFiles ()[0 ], dlg .selectedNameFilter ()
210
269
211
270
212
271
if __name__ == "__main__" : # pragma: no cover
0 commit comments